生成数据

  • 数据可视化:通过可视化表示来探索数据
  • 数据挖掘:使用代码来探索数据集的规律和关联

安装 matplotlib

pip

pip是一个以Python写成的软件包管理系统,它可以安装和管理软件包

大多数较新的Python版本都自带pip,因此首先可检查系统是否已经安装了pip。

  • 在Windows系统中检查是否安装了pip

    打开一个终端窗口,并执行如下命令:

    1
    2
    > python -m pip --version
    pip 20.1.1 from C:\Users\Hunter\AppData\Local\Programs\Python\Python38-32\lib\site-packages\pip (python 3.8)

在 Windows 系统中安装 matplotlib

在Windows系统中,首先需要安装Visual Studio。接下来,访问matplotlib · PyPI,查找与使用的Python版本、操作系统相匹配的.whl文件。

将这个.whl文件复制到项目所在文件夹,打开命令窗口,使用pip来安装matplotlib:

1
python -m pip install --user matplotlib-3.2.2-cp38-cp38-win32.whl

测试 matplotlib

安装必要的包后,对安装进行测试。为此,首先使用命令python或python3启动一个终端会话,再尝试导入matplotlib:

1
2
$ python3
>>> import matplotlib

如果没有出现任何错误消息,就说明成功安装了 matplotlib


绘制简单的折线图

我们将使用平方数序列1、4、9、16和25来绘制这个图表。

1
2
3
4
5
6
7
import matplotlib.pyplot as plt

%matplotlib inline # 让生成的图形嵌入jupyter notebook中

squares = [1, 4, 9, 16, 25]
plt.plot(squares)
plt.show()
  • 模块pyplot包含很多用于生成图表的函数。

  • plt.show()打开matplotlib查看器,并显示绘制的图形。查看器能够缩放和导航图形,另外,单击磁盘图表可将图形保存起来

修改标签文字和线条粗细

上述代码通过matplotlib查看器显示的图形,标签文字太小、线条太细。下面通过一些定制来改善这个图形的可读性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import matplotlib.pyplot as plt

squares = [1, 4, 9, 16, 25]
plt.plot(squares, linewidth=5)

# 设置图表标题,并给坐标轴加上标签
plt.title("Square Numbers", fontsize=24)
plt.xlabel("Value", fontsize=14)
plt.ylabel("Square of Value", fontsize=14)

# 设置刻度标记的大小
plt.tick_params(axis='both', labelsize=14)

plt.show()
  • 参数linewidth决定了plot()绘制的线条的粗细
  • 函数title()给图表指定标题
  • 参数fontsize指定了图表中文字的大小
  • 函数xlabel()ylabel()
  • 函数tick_params()设置刻度的样式

校正图形

图形更容易阅读后,我们发现没有正确地绘制数据:折线图的终点指出4.0的平方为25。下面来修复这个问题。

当你向plot()提供一系列数字时,它假设第一个数据点对应的x坐标值为0,但我们的第一个点对应的x值为1。为改变这种默认行为,我们可以给plot()同时提供输入值和输出值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import matplotlib.pyplot as plt

input_values = [1, 2, 3, 4, 5]
squares = [1, 4, 9, 16, 25]
plt.plot(input_values, squares, linewidth=5)

# 设置图表标题,并给坐标轴加上标签
plt.title("Square Numbers", fontsize=24)
plt.xlabel("Value", fontsize=14)
plt.ylabel("Square of Value", fontsize=14)

# 设置刻度标记的大小
plt.tick_params(axis='both', labelsize=14)

plt.show()

使用 scatter() 绘制散点图并设置其样式

有时候,需要绘制散点图设置各个数据点的样式。例如,你可能想以一种颜色显示较小的值,用另一种颜色显示较大的值。绘制大型数据集时,还可以对每个点都设置相同的样式,再使用不同的样式选项重新绘制某些点,以突出它们

要绘制简单的点,可使用函数scatter(),并向它传递一对x和y坐标,它将在指定位置绘制一个点

1
2
3
4
import matplotlib.pyplot as plt

plt.scatter(2, 4)
plt.show()

下面来设置输出的样式,使其更有趣:添加标题,给轴加上标签,并确保所有文本都大到能够看清

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import matplotlib.pyplot as plt

plt.scatter(2, 4, s=200)

# 设置图表标题并给坐标轴加上标签
plt.title("Square Numbers", fontsize=24)
plt.xlabel("Value", fontsize=14)
plt.ylabel("Square of Value", fontsize=14)

# 设置刻度标记的大小
plt.tick_params(axis='both', which='major', labelsize=14)
# 显示图例
plt.legend()

plt.show()
  • plt.scatter(2, 4, s=200)实参s设置了绘制图形时使用的点的尺寸

使用 scatter() 绘制一系列点

要绘制一系列的点,可向scatter()传递两个分别包含x值和y值的列表,如下所示:

1
2
3
4
5
6
7
8
9
import matplotlib.pyplot as plt

x_values = [1, 2, 3, 4, 5]
y_values = [1, 4, 9, 16, 25]

plt.scatter(x_values, y_values, s=100)

# 设置图表标题并给坐标轴加上标签
--snip--

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
--snip--

# 绘制一个展示 船票Fare 与 乘客年龄和性别 之间关系的散点图


# 方式一
# 利用query对性别和年龄进行筛选
x_values_1 = titanic.query("Sex == 'male' & Age != 'NaN'")['Age']
y_values_1 = titanic.query("Sex == 'male' & Age != 'NaN'")['Fare']

x_values_2 = titanic.query("Sex == 'female' & Age != 'NaN'")['Age']
y_values_2 = titanic.query("Sex == 'female' & Age != 'NaN'")['Fare']

plt.scatter(x_values_1, y_values_1, s=10, c='b', label='Male')
plt.scatter(x_values_2, y_values_2, s=10, c='r', label='Female')

# 方式二
titanic.loc[titanic['Sex'] == 'male', 'color'] = 'b' # 该处loc[]的用法为:指出 符合条件的行的列值 和 列标签
titanic.loc[~(titanic['Sex'] == 'male'), 'color'] = 'r'
plt.scatter(titanic['Age'].dropna(), titanic['Fare'].dropna(), s=10, c=titanic[color])



plt.title("船票与年龄", fontsize=24)
plt.xlabel("Age", fontsize=14)
plt.ylabel("Fare", fontsize=14)
# 显示图例
plt.legend()

plt.show()

可利用query()排除一些极端值的干扰

1
2
3
# 可视化 消费金额和购买数量的关系散点图
gu = grouped_user.sum().query('order_amount < 6000')
plt.scatter(gu['order_amount'], gu['order_products'])

自动计算数据

下面是绘制1000个点的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import matplotlib.pyplot as plt

x_values = list(range(1, 1001))
y_values = [x**2 for x in x_values]

plt.scatter(x_values, y_values, s=40)

# 设置图表标题并给坐标轴加上标签
plt.title("Square Numbers", fontsize=24)
plt.xlabel("Value", fontsize=14)
plt.ylabel("Square of Value", fontsize=14)

# 设置每个坐标轴的取值范围
plt.axis([0, 1100, 0, 1100000])

plt.show()

由于这个数据集较大,我们将点设置得较小,并使用函数axis()指定了每个坐标轴的取值范围


删除数据点的轮廓

matplotlib允许你给散点图中的各个点指定颜色,默认为蓝色点黑色轮廓。在散点图包含的数据点不多时效果很好,但绘制很多点时,黑色轮廓可能会粘连在一起

要删除数据点的轮廓,可在调用scatter()时传递实参edgecolor='none'

plt.scatter(x_values, y_values, edgecolor='none', s=40)

注意:matplotlib 2.0.0版本之后,scatter()函数的实参edgecolor默认为’none'


自定义颜色

修改数据点的颜色,可向scatter()传递参数c,并将其设置为要使用的颜色的名称,如下所示:

plt.scatter(x_values, y_values, c='red', s=40)

还可以使用RGB颜色模式自定义颜色:

plt.scatter(x_values, y_values, c=(0, 0, 0.8), s=40)

值越接近0,指定的颜色越深,值越接近1,指定的颜色越浅


使用颜色映射

颜色映射(colormap)是一系列颜色,它们从起始颜色渐变到结束颜色。在可视化中,颜色映射用于突出数据的规律,例如你可能用较浅的颜色来显示较小的值,并用较深的颜色来显示较大的值。

模块pyplot内置了一组颜色映射

1
2
3
4
5
6
7
8
9
10
import matplotlib.pyplot as plt

x_values = list(range(1, 1001))
y_values = [x**2 for x in x_values]

plt.scatter(x_values, y_values, c=y_values, cmap=plt.cm.Blues,
s=40)

# 设置图表标题并给坐标轴加上标签
--snip--

我们将参数c设置成了一个y值列表,并使用参数cmap告诉pyplot使用哪个颜色映射。y值较小的点显示为浅蓝色,y值较大的点显示为深蓝色。

注意:要了解pyplot中所有的颜色映射,访问Colormap reference


自动保存图表

要让程序自动将图表保存到文件中,可将对plt.show()的调用替换为对plt.savefig()的调用:

plt.savefig('suqares_plot.png', bbox_inches='tight')

  • 第一个实参指定要以什么样的文件名保存图表,这个文件将存储到当前程序所在的目录中
  • 第二个实参指定将图表多余的空白区域剪掉。如果要保留图表周围多余的空白区域,可省略这个实参

绘制饼图

matplotlib可视化饼图 - 知乎

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 绘制一个展示男女乘客比例的扇形图
males = (titanic['Sex'] == 'male').sum()
females = (titanic['Sex'] == 'female').sum()

proportions = [males, females]

# 绘制饼图
plt.pie(
proportions,

labels = ['Males', 'Females'],

#是否 添加阴影效果
shadow = False,

# 指定填充色
colors = ['blue', 'red'],

# 每一块偏移中心的距离
explode = (0.15 , 0.3),

# 饼图的初始摆放角度,默认图是从x轴正方向逆时针画起,如设定=90则从y轴正方向画起
startangle = 90,

# 显示百分比, 小数点前后表示最少的位数 末尾两个%%是输出%自身
autopct = '%1.1f%%'

)


# x,y轴刻度设置一致,保证饼图为圆形
plt.axis('equal')

# 设置图表标题
plt.title("男女乘客比例")

# tight_layout会自动调整子图参数,使之填充整个图像区域
plt.tight_layout()

plt.show()

随机漫步

随机漫步是这样行走得到的路径每次行走都完全是随机的,没有明确的方向,结果是由一系列随机决策决定的。在自然界、物理学、生物学、化学和经济领域,随机漫步都有其实际用途。例如,漂浮在水滴上的花粉因不断受到水分子的挤压而在水面上移动。水滴中的分子运动是随机的,因此花粉在水面上的运动路径犹如随机漫步。

创建 RandomWalk() 类

创建一个RandomWalk()类,这个类需要三个属性:

  1. 存储随机漫步次数的变量
  2. 随机漫步经过的每个点的x坐标
  3. 随机漫步经过的每个点的y坐标

RandomWalk()类只包含两个方法:

  1. __init__()
  2. fill_walk():计算随机漫步经过的所有点

先来看看__init__():

1
2
3
4
5
6
7
8
9
10
11
12
from random import choice

class RandomWalk():
"""一个生成随机漫步数据的类"""

def __init__(self, num_points=5000):
"""初始化随机漫步的属性"""
self.num_points = num_points

# 所有随机漫步都始于(0, 0)
self.x_values = [0]
self.y_values = [0]

为做出随机决策,我们将所有可能的选择都存储在一个列表中,并在每次做决策时都使用choice()来决定使用哪种选择


选择方向

我们将使用fill_walk()来生成漫步包含的点,并决定每次漫步的方向,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def fill_walk(self):
"""计算随机漫步包含的所有点"""

# 不断漫步,直到列表到达指定的长度
while len(self.x_values) < self.num_points:
# 决定前进方向以及沿这个方向前进的距离
x_direction = choice([1, -1])
x_distance = choice([0, 1, 2, 3, 4])
x_step = x_direction * x_distance

y_direction = choice([1, -1])
y_distance = choice([0, 1, 2, 3, 4])
y_step = y_direction * y_distance

# 拒绝原地踏步
if x_step == 0 and y_step == 0:
continue

# 计算下一个点的x和y值
next_x = self.x_values[-1] + x_step
next_y = self.y_values[-1] + y_step

self.x_values.append(next_x)
self.y_values.append(next_y)
  • choice([-1, 1])从-1和1中随机选择一个值
  • choice([0, 1, 2, 3, 4])从0~4中随机选择一个值

绘制随机漫步图

rw_visual.py

1
2
3
4
5
6
7
8
9
import matplotlib.pyplot as plt

from random_walk import RandomWalk

# 创建一个RandomWalk实例,并将其包含的点都绘制出来
rw = RandomWalk()
rw.fill_walk()
plt.scatter(rw.x_values, rw.y_values, s=15)
plt.show()

模拟多次随机漫步

要在不多次运行程序的情况下模拟多次随机漫步,一种办法是将这些代码放在一个while循环中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import matplotlib.pyplot as plt

from random_walk import RandomWalk

# 只要程序处于活动状态,就不断地模拟随机漫步
while True:
# 创建一个RandomWalk实例,并将其包含的点都绘制出来
rw = RandomWalk()
rw.fill_walk()
plt.scatter(rw.x_values, rw.y_values, s=15)
plt.show()

keep_running = input("Make another walk? (y/n): ")
if keep_running == 'n':
break

给点着色

我们将使用颜色映射来指出漫步中各点的先后顺序。为根据漫步中各点的先后顺序进行着色,我们传递参数c,并将其设置为一个列表,其中包含各点的先后顺序

由于这些点是按顺序绘制的,因此给参数c指定的列表只需包含数字1~5000

1
2
3
4
5
6
7
8
9
10
11
12
--snip--
while True:
# 创建一个RandomWalk实例,并将其包含的点都绘制出来
rw = RandomWalk()
rw.fill_walk()

point_numbers = list(range(rw.num_points))
plt.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues, s=15)
plt.show()

keep_running = input("Make another walk? (y/n): ")
--snip--

重新绘制起点和终点

1
2
3
4
5
6
7
8
9
10
11
--snip--
while True:
--snip--
plt.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues, s=15)

# 突出起点和终点
plt.scatter(0, 0, c='green', s=100)
plt.scatter(rw.x_values[-1], rw.y_values[-1], c='red', s=100)

plt.show()
--snip--

隐藏坐标轴

1
2
3
4
5
6
7
8
9
10
11
--snip--
while True:
--snip--
plt.scatter(rw.x_values[-1], rw.y_values[-1], c='red', s=100)

# 隐藏坐标轴
plt.axes().get_xaxis().set_visible(False)
plt.axes().get_yaxis().set_visible(False)

plt.show()
--snip--

为修改坐标轴,使用了函数plt.axes()来将每条坐标轴的可见性设置为False。


增加点数

在创建RandomWalk实例时增大num_points的值,并在绘图时调整每个点的大小,如下所示:

1
2
3
4
5
6
7
8
9
10
--snip--
while True:
# 创建一个RandomWalk实例,并将其包含的点都绘制出来
rw = RandomWalk(5000)
rw.fill_walk()

# 绘制点并将图形显示出来
point_numbers = list(range(rw.num_points))
plt.scatter(rw.x_values, rw.y_values, c=point_numbers, cmap=plt.cm.Blues, s=1)
--snip--

调整尺寸以适合屏幕

图表适合屏幕大小时,更能有效地将数据中的规律呈现出来。为让绘图窗口更适合屏幕大小,可像下面这样调整matplotlib输出的尺寸

1
2
3
4
5
6
7
8
9
--snip--
while True:
# 创建一个RandomWalk实例,并将其包含的点都绘制出来
rw = RandomWalk()
rw.fill_walk()

# 设置绘图窗口的尺寸
plt.figure(figsize=(10, 6))
--snip--

函数figure()用于指定图表的宽度、高度、分辨率和背景色

需要给形参figsize指定一个元组,向matplotlib指出绘图窗口的尺寸单位为英寸

Python假定屏幕分辨率为80像素/英寸,如果知道自己系统的分辨率,可使用形参dpi向figure()传递该分辨率,以有效利用可用的屏幕空间:

plt.figure(dpi=128, figsize=(10, 6))


使用 Pygal 模拟掷骰子

本节将使用Python可视化包Pygal来生成可缩放的矢量图形文件,它们将自动缩放,以适合观看者的屏幕。如果你打算以在线的方式使用图表,请考虑使用Pygal来生成它们,这样在任何设备上显示都会很美观。

在这个项目中,我们将对掷骰子的结果进行分析。为确定哪些点数出现的可能性最大,我们将生成一个表示掷骰子结果的数据集,并根据结果绘制出一个图形

安装 Pygal

在 Windows 系统中,命令:

python -m pip install --user pygal==1.7


Paygal 画廊

要了解使用Pygal可创建什么样的图表,可以查看Chart types — pygal documentation


创建 Die 类

下面的类模拟掷一个骰子:

1
2
3
4
5
6
7
8
9
10
11
12
from random import randint

class Die():
"""表示一个骰子的类"""

def __init__(self, num_sides=6):
"""骰子默认为6面"""
self.num_sides = num_sides

def roll(self):
"""返回一个位于1和骰子面数之间的随机数"""
return randint(1, self.num_sides)
  • 方法roll()使用函数randint()返回一个1和面数之间的随机整数(包括起始值1和终止值num_sides)

骰子根据面数命名,6面的骰子命名为D6,8面的骰子命名为D8。


掷骰子

使用这个类来创建图表前,先来掷D6骰子,将结果打印出来,并检查结果是否合理:

1
2
3
4
5
6
7
8
9
10
11
12
from die import Die

# 创建一个D6
die = Die()

# 掷几次骰子,并将结果存储在一个列表中
results = []
for roll_num in range(100):
result = die.roll()
results.append(result)

print(results)

分析结果

为分析掷一个D6骰子的结果,我们计算每个点数出现的次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
--snip--
# 掷几次骰子,并将结果存储在一个列表中
results = []
for roll_num in range(1000):
result = die.roll()
results.append(result)

# 分析结果
frequencies = []
for value in range(1, die.num_sides+1):
frequency = results.count(value)
frequencies.append(frequency)

print(frequencies)
  • 为分析结果,我们创建了空列表frequencies,用于存储每种点数出现的次数

绘制条形图(bar chart)

有了频率列表后,我们就可以绘制一个表示结果的条形图(bar chart)指出各种结果出现的频率

  1. 利用pygal绘制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pygal
--snip--

# 分析结果
frequencies = []
for value in range(1, die.num_sides+1):
frequency = results.count(value)
frequencies.append(frequency)

# 对结果进行可视化
bar = pygal.Bar()

bar.title = "Results of rolling one D6 1000 times."
bar.x_labels = ['1', '2', '3', '4', '5', '6']
bar.x_title = "Result"
bar.y_title = "Frequency of Result"

bar.add('D6', frequencies)
bar.render_to_file('die_visual.svg')
  • 为创建**条形图(bar diagram)**,创建了一个pygal.Bar()实例
  • 将D6骰子的可能结果用作x轴的标签
  • 使用add()将一系列值添加到图表中(传递给添加的值指定的标签将出现在图表中的值的列表)
  • 将图表渲染为一个SVG文件(Scalable Vector Graphics,可缩放的矢量图形,是一种基于XML图像文件格式)

要查看生成的直方图,最简单的方式是使用Web浏览器。为此,在任意浏览器中新建一个标签页,再打开文件die_visual.svg,将看到对应的图表。

注意:Python让这个图表具有交互性:如果将鼠标指向该图表中的任何条形,将看到与之相关联的数据(若无效果,刷新)。


绘制直方图(histogram)

  1. 利用pyplot绘制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import pandas as pd
import matplotlib.pyplot as plt
# 绘制一个展示船票价格的直方图

# 创建直方图
# 参数:需要计算的series ,bins可以是直方图个数(默认为10),也可以是自定义的序列
plt.hist(titanic['Fare'], bins = range(0,600,10))

# Set the title and labels
plt.xlabel('Fare')
plt.ylabel('Frequency')
plt.title('船票价格分布')

# show the plot
plt.show()


# 用户消费金额的分布图,利用query消除极值对可视化呈现效果的干扰
plt.hist(grouped_user.sum().query('order_amount < 1000')['order_amount'], bins = 20)
plt.show()

同时掷两个骰子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import pygal

from die import Die

# 创建两个D6骰子
die_1 = Die()
die_2 = Die()

# 掷骰子多次,并将结果存储到一个列表中
results = []
for roll_num in range(1000):
result = die_1.roll() + die_2.roll()
results.append(result)

# 分析结果
frequencies = []
max_result = die_1.num_sides + die_2.num_sides
for value in range(2, max_result+1):
frequency = results.count(value)
frequencies.append(frequency)

# 可视化结果
bar = pygal.Bar()

bar.title = "Results of rolling two D6 dice 1000 times."
bar.x_labels = ['2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12']
bar.x_title = "Result"
bar.y_title = "Frequency of Result"

bar.add('D6 + D6', frequencies)
bar.render_to_file('dice_visual.svg')

同时掷两个面数不同的骰子

下面来创建一个6面骰子和一个10面骰子,看看同时掷这两个骰子50000次的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from die import Die

import pygal

# 创建一个D6和一个D10
die_1 = Die()
die_2 = Die(10)

# 掷骰子多次,并将结果存储到一个列表中
results = []
for roll_num in range(50000):
result = die_1.roll() + die_2.roll()
results.append(result)

# 分析结果
frequencies = []
max_result = die_1.num_sides + die_2.num_sides
for value in range(2, max_result+1):
frequency = results.count(value)
frequencies.append(frequency)

# 可视化结果
bar = pygal.Bar()

bar.title = "Results of rolling a D6 and a D10 50,000 times."
bar.x_labels = [str(value) for value in range(2, 17)]
bar.x_title = "Result"
bar.y_title = "Frequency of Result"

bar.add('D6 + D10', frequencies)
bar.render_to_file('dice_visual.svg')
  • 通过列表解析调整x轴标签

下载数据

网上的数据多得难以置信,且大多未经过仔细检查。如果能够对这些数据进行分析,你就能发现别人没有发现的规律和关联。

CSV 文件格式

要在文本文件中存储数据,最简单的方式是将数据作为一系列以逗号分隔的值(CSV, Comma-Separated Values)写入文件,这样的文件称为CSV文件

例如,下面是一行CSV格式的天气数据:

2014-1-5,61,44,26,18,7,-1,56,30,9,30.34,30.27,30.15,,,,10,4,,0.00,0,,195

CSV文件对人来说阅读起来比较麻烦,但程序可轻松地提取并处理其中的值,这有助于加快数据分析过程。

  • 最好使用用文字编辑器打开csv文件查看Excel可能会更改显示格式

分析 CSV 文件头

csv模块包含在Python标准库中,可用于分析CSV文件中的数据行,让我们能够快速提取感兴趣的值。下面先来看一下sitka_weather_07-2014.csv文件的第一行,其中包含一系列有关数据的描述

1
2
3
4
5
6
7
import csv

filename = 'sitka_weather_07-2014.csv'
with open(filename) as f:
reader = csv.reader(f)
header_row = next(reader)
print(header_row)
  • 调用csv.reader(),并将前面存储的文件对象作为实参传入,从而创建一个与该文件相关联的阅读器对象
  • 模块csv的reader类包含next()方法,调用内置函数next()将一个阅读器对象作为参数传入,将调用阅读器对象的next()方法,从而返回文件中的下一行
  • reader处理文件中以逗号分隔的第一行数据,并将每项数据都作为一个元素存储在列表中

注意:文件头的格式并非总是一致的, 空格和单位可能出现在奇怪的地方,但不会带来任何问题


打印文件头及其位置

为让文件头数据更容易理解,将列表中的每个文件头及其位置打印出来:

1
2
3
4
5
6
7
--snip--
with open(filename) as f:
reader = csv.reader(f)
header_row = next(reader)

for index, column_header in enumerate(header_row):
print(index, column_header)
  • 对列表调用了enumerate()(枚举)来获取每个元素的索引及其值

提取并读取数据

首先读取每天的最高气温

1
2
3
4
5
6
7
8
9
10
11
12
13
import csv

# 从文件中获取最高气温
filename = 'sitka_weather_07-2014.csv'
with open(filename) as f:
reader = csv.reader(f)
header_row = next(reader)

highs = []
for row in reader:
highs.append(row[1])

print(highs)
  • for row in reader遍历文件中余下的各行

    每次执行上述循环,我们都将索引1处(第2列)的数据附加到highs末尾。

下面使用int()将这些字符串转换为数字让matplotlib能够读取它们

1
2
3
4
5
6
7
--snip--
highs = []
for row in reader:
high = int(row[1])
highs.append(high)

print(highs)

绘制气温图表

为可视化这些气温数据,我们首先使用matplotlib创建一个显示每日最高气温的简单图形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import csv

from matplotlib import pyplot as plt

# 从文件中获取最高气温
--snip--

# 根据数据绘制图形
plt.figure(dpi=128, figsize=(10, 6))
plt.plot(highs, c='red')

# 设置图形的格式
plt.title("Daily high temperatures, July 2014", fontsize=24)
plt.xlabel('', fontsize=16)
plt.ylabel("Temperature (F)", fontsize=16)
plt.tick_params(axis='both', which='major', labelsize=16)

plt.show()
  • 鉴于还没有添加日期,因此没有给x轴添加标签

  • tick_params()

    which一共3个参数:[‘major’ , ‘minor’. ‘both’]
    默认是major,表示主刻度,后面分布为次刻度主次刻度都显示.

  • 调整x轴日期的显示间隔

    1
    plt.xticks(pd.date_range('1997-01-01', '1997-04-01', freq='10d'))

模块 datetime

下面在图表中添加日期。在天气数据文件中,第一个日期在第二行

获取日期数据时,获得的是一个字符串,因此需要将字符串'2014-7-1'转换为一个表示相应日期的对象。可使用模块datetime中的方法strptime()

1
2
3
4
>>> from datetime import datetime
>>> first_date = datetime.strptime('2014-7-1', '%Y-%m-%d')
>>> print(first_date)
2014-07-01 00:00:00
  • 传入方法strptime()第2个实参告诉Python如何设置日期的格式

    • %Y-让Python将字符串中第一个连字符前面的部分视为4位的年份
    • %m-让Python将第二个连字符前面的部分视为表示月份的数字
    • %d让Python将最后一部分视为月份中的一天
  • 方法strptime()可接受各种实参,并根据它们来决定如何解读日期:

    实参 含义
    %A 星期的名称,如Monday
    %B 月份名,如January
    %m 数字表示的月份 (01~12)
    %d 数字表示月份中的一天 (01~31)
    %Y 四位的年份,如2015
    %y 两位的年份,如15
    %H 24小时制的小时数 (00-23)
    %I 12小时制的小时数 (01~12)
    %p am或pm
    %M 分钟数 (00~59)
    %S 秒数 (00~61)

在图表中添加日期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import csv
from datetime import datetime

from matplotlib import pyplot as plt

# 从文件中获取日期和最高气温
filename = 'sitka_weather_07-2014.csv'
with open(filename) as f:
reader = csv.reader(f)
header_row = next(reader)

dates, highs = [], []
for row in reader:
current_date = datetime.strptime(row[0], "%Y-%m-%d")
dates.append(current_date)

high = int(row[1])
highs.append(high)

# 根据数据绘制图形
plt.figure(dpi=128, figsize=(10, 6))
plt.plot(dates, highs, c='red')

# 设置图形的格式
plt.title("Daily high temperatures, July 2014", fontsize=24)
plt.xlabel('', fontsize=16)
plt.gcf().autofmt_xdate() # 绘制斜的日期标签,以免它们彼此重叠
plt.ylabel("Temperature (F)", fontsize=16)
plt.tick_params(axis='both', which='major', labelsize=16)

plt.show()
  • 创建了两个空列表:dates, highs = [], []
  • 将包含日期信息的数据转换为datetime对象:datetime.strptime(row[0], "%Y-%m-%d")
  • 将日期和最高气温值传给plot()plt.plot(dates, highs, c='red')

涵盖更长的时间

创建覆盖整年的天气图:

1
2
3
4
5
6
7
8
9
--snip--
# 从文件中获取日期和最高气温
filename = 'stika_weather_2014.csv'
with open(filename) as f:
--snip--
# 设置图形的格式
plt.title("Daily high temperatures - 2014", fontsize=24)
plt.xlabel('', fontsize=16)
--snip--

再绘制一个数据系列

从数据文件中提取最低气温,并添加到图表中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
--snip--
# 从文件中获取日期、最高气温和最低气温
filename = 'sitka_weather_2014.csv'
with open(filename) as f:
reader = csv.reader(f)
header_row = next(reader)

dates, highs, lows = [], [], []
for row in reader:
current_date = datetime.strptime(row[0], "%Y-%m-%d")
dates.append(current_date)

high = int(row[1])
highs.append(high)

low = int(rpw[3])
lows.append(low)

# 根据数据绘制图形
plt.figure(dpi=128, figsize=(10, 6))
plt.plot(dates, highs, c='red')
plt.plot(dates, lows, c='blue')

# 设置图形的格式
plt.title("Daily high and low temperatures - 2014", fontsize=24)
--snip--

给图表区域着色

通过着色来呈现每天的气温范围,为此使用方法fill_between(),它接受一个x值系列和两个y值系列,并填充两个y值系列之间的空间

1
2
3
4
5
6
--snip--
# 根据数据绘制图形
fig = plt.figure(dpi=128, figsize=(10, 6))
plt.plot(dates, highs, c='red'. alpha=0.5)
plt.plot(dates, lows, c='blue'. alpha=0.5)
plt.fill_between(dates, highs, lows, facecolor='blue', alpha=0.1)
  • 实参alpha指定颜色的透明度
    • 0表示完全透明
    • 1(默认)表示完全不透明
  • fill_between()
    • 实参facecolor指定了填充区域的颜色

错误检查

我们应该能够使用有关任何地方的天气数据来运行highs_lows.py中的代码,但有些气象站会偶尔出现故障,未能收集部分或全部其应该收集的数据缺失数据可能会引发异常,如果不妥善地处理,还可能导致程序崩溃

例如,我们来看看生成加利福尼亚死亡谷的气温图时出现的情况:

1
2
3
4
5
--snip--
# 从文件中获取日期、最高气温和最低气温
filename = 'death_valley_2014.csv'
withj open(filename) as f:
--snip--

运行结果:

1
2
3
4
Traceback (most recent call last):
File "highs_lows.py", line 17, in <module>
high = int(row[1])
ValueError: invalid literal for int() with base 10: ''

查看文件可知,没有记录2014年2月16日的数据,表示最高温度的字符串为空。为解决这种问题,我们从CSV文件中读取值时执行错误检查代码,对分析数据集时可能出现的异常进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
--snip--
# 从文件中获取日期、最高气温和最低气温
filename = 'death_valley_2014.csv'
with open(filename) as f:
reader = csv.reader(f)
header_row = next(reader)

dates, highs, lows = [], [], []
for row in reader:
try:
current_date = datetime.strptime(row[0], "%Y-%m-%d")
high = int(row[1])
low = int(row[3])
except ValueError:
print(current_date, 'missing data')
else:
dates.append(current_date)
highs.append(high)
lows.append(low)

# 根据数据绘制图形
--snip--

# 设置图形的格式
title = "Daily high and low temperatures - 2014\nDeath Valley, CA"
plt.title(title, fontsize=20)
--snip--

在有些情况下,需要使用continue来跳过一些数据,或者使用remove()(删除列表值)或del(删除 键-值 对)将已提取的数据删除。可采用任何管用的方法,只要能进行精确而有意义的可视化就好。


16-2 比较锡特卡和死亡谷的气温:

为准确地比较锡特卡和死亡谷的气温范围,在y轴上使用相同的刻度,对两地的气温范围进行直接比较:

  • pyplot的方法ylim()可以对y轴的刻度做限制xlim()则可以对x轴的刻度做限制。
1
2
3
4
5
# 设置图形的格式
--snip--
plt.ylim(10, 120)

plt.show()

尝试在一个图表中呈现这两个数据集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import csv
from datetime import datetime

from matplotlib import pyplot as plt

def get_weather_data(filename, dates, highs, lows):
"""从文件中获取日期、最高气温和最低气温"""
with open(filename) as f:
reader = csv.reader(f)
header_row = next(reader)

for row in reader:
try:
current_date = datetime.strptime(row[0], "%Y-%m-%d")
high = int(row[1])
low = int(row[3])
except ValueError:
print(current_date, 'missing data')
else:
dates.append(current_date)
highs.append(high)
lows.append(low)

# 获取锡特卡的气温数据
dates, highs, lows = [], [], []
get_weather_data('stika_weather_2014.csv', dates, highs, lows)

# 根据锡特卡数据绘制图形
plt.figure(dpi=128, figsize=(10, 6))
plt.plot(dates, highs, c='red', alpha=0.6)
plt.plot(dates, lows, c='blue', alpha=0.6)
plt.fill_between(dates, highs, lows, facecolor='blue', alpha=0.15)

# 获取死亡谷的气温数据
dates, highs, lows = [], [], []
get_weather_data('death_valley_2014.csv', dates, highs, lows)

# 将死亡谷数据的图形 添加到当前的绘制中
plt.plot(dates, highs, c='red', alpha=0.3)
plt.plot(dates, lows, c='blue', alpha=0.3)
plt.fill_between(dates, highs, lows, facecolor='blue', alpha=0.05)

# 设置图形的格式
title = "Daily high and low temperatures - 2014"
title += "\nSitka, AK and Death Valley, CA"
plt.title(title, fontsize=20)
plt.xlabel('', fontsize=16)
plt.gcf().autofmt_xdate()
plt.ylabel("Temperature (F)", fontsize=16)
plt.tick_params(axis='both', which='major', labelsize=16)
plt.ylim(10, 120)

plt.show()

制作交易收盘价走势图: JSON 格式

下载收盘价数据

btc_close_2017.json实际是一个很长的Python列表,其中每个元素都是一个包含五个键的字典:统计日期、月份、周数、周几以及收盘价。由于2017年1月1日是周日,作为2017年的第一周实在太短,。因此被计入2016年的第52周。于是2017年的第一周是从1月2日开始的。

可以将收盘价数据文件直接下载到程序所在的文件夹中,也可以用Python 2.x标准库中模块urllib2(Python 3.x版本使用urllib)的函数urlopen()来做,还可以通过Python的第三方模块requests下载数据。

如果采用函数urlopen()来下载数据,可以使用如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from __future__ import (absolute_import, division, print_function, unicode_literals)

try:
# Python 2.x 版本
from urllib2 import urlopen
except ImportError:
# Python 3.x 版本
from urllib.request import urlopen
import json

json_url = 'https://raw.githubusercontent.com/muxuezi/btc/master/btc_close_2017.json'
response = urlopen(json_url)
# 读取数据
req = response.read()
# 将数据写入文件
with open('btc_close_2017_urllib.json', 'wb') as f:
f.write(req)
# 加载json格式
file_urllib = json.loads(req)
print(file_urllib)
  • python打开文件时w与wb的区别,r与rb的区别

    Windows中的换行符\r\n

    • 文本方式('w')写入,遇到\n自动替换\r\n
    • 二进制文本方式('wb')写入,遇到\n仍然按\n记录
  • json

    • load()

      载入json文件

    • loads()

      载入JSON格式的字符串

函数urlopen的代码稍微复杂一些,第三方模块requests封装了许多常用的方法,让数据下载和读取方式变得非常简单

1
2
3
4
5
6
7
8
import requests

json_url = 'https://raw.githubusercontent.com/muxuezi/btc/master/btc_close_2017.json'
req = requests.get(json_url)
# 将数据写入文件
with open('btc_close_2017_request.json', 'w') as f:
f.write(req.text)
file_requests = req.json()
  • req.text属性可以直接读取文件数据返回格式是字符串
  • req.json()可以将json文件的数据转换为Python列表,与之前的file_urllib内容相同

提取相关的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import json

# 将数据加载到一个列表中
filename = 'btc_close_2017.json'
with open(filename) as f:
btc_data = json.load(f)
# 打印每一天的信息
for btc_dict in btc_data:
date = btc_dict['date']
month = btc_dict['month']
week = btc_dict['week']
weekday = btc_dict['weekday']
close = btc_dict['close']
print("{} is month {} week {}, {}, the close price is {} RMB".format(date, month, week, weekday, close))

将字符串转换为数字值

btc_close_2017.json中的每个键和值都是字符串。为了能在后边的内容中对交易数据进行计算,需要先将表示周数和收盘价的字符串转换为数值,因此使用函数int()

1
2
3
4
5
6
7
8
9
10
--snip--

# 打印每一天的信息
for btc_dict in btc_data:
date = btc_dict['date']
month = int(btc_dict['month'])
week = int(btc_dict['week'])
weekday = btc_dict['weekday']
close = int(float(btc_dict['close']))
print("{} is month {} week {}, {}, the close price is {} RMB".format(date, month, week, weekday, close))
  • 在实际工作中,原始数据的格式经常是不统一的。此类数值类型转换造成的ValueError异常十分普遍。该例子中,无法将包含小数点的字符串转换为整数,因此需要先转换为浮点数再转换为整数int(float(btc_dict['close']))

有了这些数据之后,可以结合Pygal的可视化功能来探索一些有趣的信息


绘制收盘价折线图

之前章节介绍过用Pygal绘制条形图(bar chart)的方法,也介绍了用matplotlib绘制折线图(line chart)的方法。下面用Pygal来实现收盘价的折线图

绘制折线图之前,需要获取x轴与y轴数据,因此我们创建了几个列表来存储数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--snip--

# 创建5个列表,分别存储日期和收盘价
dates = []
months = []
weeks = []
weekdays = []
close = []
# 每一天的信息
for btc_dict in btc_data:
dates.append(btc_dict['date'])
months.append(int(btc_dict['month']))
weeks.append(int(btc_dict['week']))
weekdays.append(btc_dict['weekday'])
close.append(int(float(btc_dict['close'])))

由于数据点比较多,x轴要显示346个日期,因此需要利用Pygal的配置参数,对图形进行适当的调整

1
2
3
4
5
6
7
8
9
10
11
--snip--

import pygal

line_chart = pygal.Line(x_label_rotation=20, show_mirror_x_labels=False)
line_chart.title = '收盘价(¥)'
line_chart.x_labels = dates
N = 20 # x轴坐标每隔20天显示一次
line_chart.x_labels_major = dates[::N]
line_chart.add('收盘价', close)
line_chart.render_to_file('收盘价折线图(¥).svg')
  • pygal.Line()创建Line实例
    • x_label_rotation=20:让x轴上的标签顺时针旋转20°
    • show_mirror_x_labels=False:告诉图形不用显示所有的x轴标签
  • line_chart.x_labels_major = dates[:20]:x轴坐标每隔20天显示一次

下面对价格做一些简单的探索


时间序列特征初探

进行时间序列分析总是期望发现趋势(trend)周期性(seasonality)噪声(noise),从而能够描述事实、预测未来、做出决策

从收盘价的折线图可以看出,2017年的总体趋势时非线性的,而且增长幅度不断增大,似乎呈指数分布。但是,在每个季度末(3月、6月、9月)似乎有一些相似的波动为了验证周期性的假设,需要先将非线性的趋势(指数增长部分)消除。**对数变换(log transformation)**是常用的处理方法之一。

Python标准库的数学模块math来解决这个问题。math里有许多常用的数学函数,这里以10为底的对数函数math.log10计算收盘价,日期仍然保持不变——这种方式称为半对数(semi-logarithmic)变换

1
2
3
4
5
6
7
8
9
10
11
12
13
--snip--

import pygal
import math

line_chart = pygal.Line(x_label_rotation=20, show_mirror_x_labels=False)
line_chart.title = '收盘价对数变换(¥)'
line_chart.x_labels = dates
N = 20 # x轴坐标每隔20天显示一次
line_chart.x_labels_major = dates[::N]
close_log = [math.log10(_) for _ in close]
line_chart.add('log收盘价', close_log)
line_chart.render_to_file('收盘价对数变换折线图(¥).svg')

剔除非线性趋势之后,整体的趋势更接近线性增长,且收盘价在每个季度末似乎有显著的周期性。那么,12月会不会再现这一场景?下面来看看收盘价的月日均值周日均值的表现。


收盘价均值

再利用btc_close_2017.json文件中的数据,绘制2017年前11个月的日均值、前49周(01-02~12-10)的日均值,以及每周各天的日均值。虽然这些日均值的数值不同,但都是一段时间的均值,计算方法都是一样的。因此,可以将绘图代码封装成函数,以便重复使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
--snip--

from itertools import groupby

def draw_line(x_data, y_data, title, y_legend):
xy_map = []
for x, y in groupby(sorted(zip(x_data, y_data)), key=lambda _: _[0]):
y_list = [v for _, v in y]
xy_map.append([x, sum(y_list) / len(y_list)])
x_unique, y_mean = [*zip(*xy_map)]
line_chart = pygal.Line()
line_chart.title = title
line_chart.x_labels = x_unique
line_chart.add(y_legend, y_mean)
line_chart.render_to_file(title+'.svg')
return line_chart
  • 由于需要将数据按月份、周数、周几分组,再计算每组的均值,因此导入Python标准库中模块itertools的函数groupby
  • 将x轴与y轴的数据合并排序,再用函数groupby分组
  • 分组之后,求出每组的平均值,存储到xy_map中
  • 最后,将xy_map中存储的x轴与y轴数据分离,就可以像之前一样用Pygal画图了

下面画出收盘价月日均值,由于12月的数据不完整,只取1月到11月的数据。通过dates查找2017-12-01索引的位置,确定周数和收盘价的取值范围

1
2
3
4
idx_month = dates.index('2017-12-01')
line_chart_month = draw_line(months[:idx_month], close[:idx_month], '收盘价月日均值(¥)',
'月日均值')
line_chart_month

收盘价数据仪表盘

每个SVG文件打开之后都是独立的页面,如果能整合在一起,就可以方便地进行长期管理、监测和分析。另外,新的图表也可以方便地加入进来,这样就形成了一个数据仪表盘(dashboard)。下面,将之前绘制的图整合起来,做一个收盘价数据仪表盘:

1
2
3
4
5
6
7
8
9
10
11
12
--snip--

with open('收盘价Dashboard.html', 'w', encoding='utf8') as html_file:
html_file.write(
'<html><head><title>收盘价Dashboard</title><meta charset="utf-8"></head><body>\n')
for svg in [
'收盘价折线图(¥).svg', '收盘价对数变换折线图(¥).svg', '收盘价月日均值(¥).svg',
'收盘价周日均值(¥).svg', '收盘价星期均值(¥).svg'
]:
html_file.write(
' <object type="image/svg+xml" data="{0}" height=500></object>\n'.format(svg))
html_file.write('</body></html>')

和常见网络应用的数据仪表盘一样,这个数据仪表盘也是一个完整的网页。


使用 API

使用 Web API

Web API是网站的一部分,用于 与使用非常具体的URL请求特定信息的程序 交互。这种请求称为API调用。请求的数据将以易于处理的格式(如JSON或CSV)返回

依赖于外部数据源的大多数应用程序都依赖于API调用,如集成社交媒体网站的应用程序。

Git 和 GitHub

本章的可视化将基于来自GitHub的信息。我们将使用GitHub的API来请求有关该网站中Python项目的信息,然后使用Pygal生成交互式可视化,以呈现这些项目的受欢迎程度

本章将编写一个程序,它自动下载GitHub上星级最高的Python项目的信息,并对这些信息进行可视化


使用 API 调用请求数据

GitHub的API让你能够通过API调用来请求各种信息。要知道API调用是什么样的,在浏览器中访问如下地址:

1
https://api.github.com/search/repositories?q=language:python&sort=starshttps://api.github.com/
  • 第一部分https://api.github.com/:将请求发送到GitHub网站中响应API调用的部分
  • 接下来的部分search/repositories:让API搜索GitHub上的所有仓库
  • repositories后面的问号指出我们要传递一个实参
    • q=q表示查询等号让我们能够开始指定查询
    • 通过使用language:python,我们指出只想获取主要语言为Python的仓库的信息
  • 最后一部分&sort=stars:指定将项目按其获得的星级排序

安装 requests

requests包让Python程序能够轻松地向网站请求信息以及检查返回的响应。要安装requests,执行:

1
pip install --user requests

处理 API 响应

下面来编写一个程序,它执行API调用并处理结果,找出GitHub上星级最高的Python项目:

1
2
3
4
5
6
7
8
9
10
11
12
import requests

# 执行API调用并存储响应
url = 'https://api.github.com/search/repositories?=language:python&sort=stars'
r = requests.get(url)
print("Status code:", r.status_code)

# 将API响应存储在一个变量中
response_dict = r.json()

# 处理结果
print(response_dict.keys())
  • 使用requests.get()执行API调用

    响应对象包含一个名为status_code的属性

    • 状态码200表示请求成功
  • 这个API返回JSON格式的信息。因此我们使用方法json()将这些信息转换为一个Python字典


处理响应字典

下面来生成一些概述API调用返回的信息的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests

# 执行API调用并存储响应
url = 'https://api.github.com/search/repositories?q=language:python&sort=stars'
r = requests.get(url)
print("Status code:", r.status_code)

# 将API响应存储在一个变量中
response_dict = r.json()
print("Total repositories:", response_dict['total_count'])

# 探索有关仓库的信息
repo_dicts = response_dict['items']
print("Repositories returned:", len(repo_dicts))

# 研究第一个仓库
repo_dict = repo_dicts[0]
print("\nKeys:", len(repo_dict))
for key in sorted(repo_dict.keys()):
print(key)
  • 'items'关联的值是一个列表,其中包含很多字典,每个字典都包含有关一个Python仓库的信息

下面来提取repo_dict中与一些键相关联的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
--snip--

# 研究第一个仓库
repo_dict = repo_dicts[0]

print("\nSelected information about first repository:")
print('Name:', repo_dict['name'])
print('Owner:', repo_dict['owner']['login'])
print('Stars:', repo_dict['stargazers_count'])
print('Reposiory:', repo_dict['html_url'])
print('Created:', repo_dict['created_at'])
print('Updated:', repo_dict['updated_at'])
print('Description:', repo_dict['description'])

概述最受欢迎的仓库

对这些数据进行可视化时,我们需要涵盖多个仓库。下面就来编写一个循环,打印API调用返回的每个仓库的特定信息,以便能够在可视化中包含所有这些信息:

1
2
3
4
5
6
7
8
9
10
11
12
--snip--
# 研究有关仓库的信息
repo_dicts = response_dict['items']
print("Repositories returned:", len(repo_dicts))

print("\nSelected information about each repository:")
for repo_dict in repo_dicts:
print('\nName:', repo_dict['name'])
print('Owner:', repo_dict['owner']['login'])
print('Stars:', repo_dict['stargazers_count'])
print('Reposiory:', repo_dict['html_url'])
print('Description:', repo_dict['description'])

监视 API 的速率限制

大多数API都存在速率限制,即你在特定时间内可执行的请求数存在限制。要获悉你是否接近了GitHub的限制,在浏览器中输入https://api.github.com/rate_limit,将得到类似于下面的响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"resources":{
"core":{
"limit":60,
"remaining":0,
"reset":1593698255
},
"graphql":{
"limit":0,
"remaining":0,
"reset":1593700880
},
"integration_manifest":{
"limit":5000,
"remaining":5000,
"reset":1593700880
},
"search":{
"limit":10,
"remaining":10,
"reset":1593697340
}
},
"rate":{
"limit":60,
"remaining":0,
"reset":1593698255
}
}

我们关心的信息是搜索API的速率限制。从18~22行可知,极限为每分钟10个请求;当前这一分钟内,还可执行10个请求;reset值指的是配额将重置的Unix时间或新纪元时间(1970-01-01午夜后多少秒)。用完配额后,将收到一条简单的响应,由此知道已到达API极限。到达极限后必须等待配额重置

注意:很多API都要求你注册获得密钥后才能执行API调用。GitHub没有这样的要求,但获得API密钥后,配额将高得多。


使用 Pygal 可视化仓库

我们来进行可视化,呈现GitHub上Python项目的受欢迎程度。创建一个交互式条形图,条形的高度表示项目获得了多少颗星。单击条形将进入项目在GitHub上的主页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import requests
import pygal
from pygal.style import LightColorizedStyle as LCS, LightenStyle as LS

# 执行API调用并存储响应
url = 'https://api.github.com/search/repositories?q=language:python&sort=stars'
r = requests.get(url)
print("Status code:", r.status_code)

# 将API响应存储在一个变量中
response_dict = r.json()
print("Total repositories:", response_dict['total_count'])

# 研究有关仓库的信息
repo_dicts = response_dict['items']

names, stars = [], []
for repo_dict in repo_dicts:
names.append(repo_dict['name'])
stars.append(repo_dict['stargazers_count'])

# 可视化
my_style = LS('#333366', base_style=LCS)
chart = pygal.Bar(style=my_style, x_label_rotation=45, show_legend=False)
chart.title = 'Most-Starred Python Projects on GitHub'
chart.x_labels = names

chart.add('', stars)
chart.render_to_file('python_repos.svg')
  • LS('#333366', base_style=LCS):使用LightenStyle(别名LS)定义了一种样式,并将基色设置为深蓝色;还传递了实参base_style,以使用LightColorizedStyle类(别名LCS)
  • show_legend=False隐藏了图例,因为只在图表中绘制一个数据系列
  • 由于不需要给这个数据系列添加标签,因此添加数据时,将标签设置成了空字符串

改进 Pygal 图表

我们将进行多个方面的定制,因此先来稍微调整代码结构,创建一个配置对象,其中包含要传递给Bar()的所有定制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
--snip--

# 可视化
my_style = LS('#333366', base_style=LCS)

my_config = pygal.Config()
my_config.x_label_rotation = 45
my_config.show_legend = False
my_config.title_font_size = 24
my_config.label_font_size = 14
my_config.major_label_font_size = 18
my_config.truncate_label = 15
my_config.show_y_guides = False
my_config.width = 1000

chart = pygal.Bar(my_config, style=my_style)
chart.title = 'Most-Starred Python Projects on GitHub'
chart.x_labels = names

chart.add('', stars)
chart.render_to_file('python_repos.svg')
  • 9~11行设置了图表标题、副标签和主标签的字体大小。

    这个图表中副标签是x轴上的项目名和y轴上的大部分数字;主标签是y轴上为5000整数倍的刻度,这些标签更大,以与副标签区分开。

  • truncate_label=15将较长的项目缩短为15个字符(将鼠标指向被截短的项目名,将显示完整的项目名)

  • show_y_guides=False隐藏图表中的水平线

  • width=1000:自定义的宽度,让图表更充分地利用浏览器中的可用空间


添加自定义工具提示

在Python中,将鼠标指向条形将显示它表示的信息,这通常称为工具提示。下面来创建一个自定义工具提示,以同时显示项目的描述。

来看一个简单的示例,它可视化前三个项目,并给每个项目对应的条形都指定自定义标签。为此,我们向add()传递一个字典列表,而不是值列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import pygal
from pygal.style import LightColorizedStyle as LCS, LightenStyle as LS

my_style = LS('#333366', base_style=LCS)
chart = pygal.Bar(style=my_style, x_label_rotation=45, show_legend=False)

chart.title = 'Python Projects'
chart.x_labels = ['httpie', 'django', 'flask']

plot_dicts = [
{'value': 16101'label': 'Description of httpie.'},
{'value': 15028, 'label': 'Description of django.'},
{'value': 14798, 'label': 'Description of flask.'},
]

chart.add('', plot_dicts)
chart.render_to_file('bar_descriptions.svg')
  • plot_dicts列表包含三个字典,'value'对应的值确定条形高度'label'对应的值给条形创建工具提示

根据数据绘图

为根据数据绘图,我们将自动生成plot_dicts。其中包含API调用返回的30个项目的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
--snip--
# 研究有关仓库的信息
repo_dicts = response_dict['items']
print("Number of items:", len(repo_dicts))

names, plot_dicts = [], []
for repo_dict in repo_dicts:
names.append(repo_dict['name'])

plot_dict = {
'value': repo_dict['stargazers_count'],
'label': repo_dict['description'],
}
plot_dicts.append(plot_dict)

# 可视化
my_style = LS('#333366', base_style=LCS)
--snip--

chart.add('', plot_dicts)
chart.render_to_file('python_repos.svg')

在图表中添加可单击的链接

Pygal还允许将图表中的每个条形用作网站的链接。为此,在为每个项目创建的字典中,添加一个键为'xlink'的 键-值 对:

1
2
3
4
5
6
7
8
9
10
11
12
--snip--
names, plot_dicts = [], []
for repo_dict in repo_dicts:
names.append(repo_dict['name'])

plot_dict = {
'value': repo_dict['stargazers_count'],
'label': repo_dict['description'],
'xlink': repo_dict['html_url'],
}
plot_dicts.append(plot_dict)
--snip--

单击图表中的任何条形,都将在浏览器中打开一个新的标签页,并在其中显示相应项目的GitHub页面。


Hacker News API

为探索如何使用其他网站的API调用,我们来看看

Hacker News网站,用户分享编程和技术方面的文章,并就这些文章展开积极的讨论。Hacker News的API让你能够访问有关该网站所有文章和评论的信息,且不要求通过注册获得密钥。

已被墙。


问题

Jupyter Notebook 用matplotlib作图显示中文乱码

1
plt.rc('font', family='SimHei', size=7)  ##显示中文,字体大小根据需要调整

存在极端值干扰

  • 使用query()做范围的筛选

  • 根据切比雪夫定理做筛选:

    • 所有数据中,至少有75%的数据位于平均数2个标准差范围内
  • 所有数据中,至少有88.9%的数据位于平均数3个标准差范围内

    • 所有数据中,至少有96%的数据位于平均数5个标准差范围内

条形图和直方图的区别

  • 条形图(bar chart)

    1. 条形的高度表示各类别频数的多少,其宽度(表示类别)则是固定的;
    2. 条形图各矩形分开排列(有间隙)
    3. 主要用于展示分类数据
  • 直方图(Histogram)

    1. 面积表示各组频数的多少,矩形的高度表示每一组的频数或频率宽度则表示各组的组距
    2. 分组数据具有连续性,直方图各矩形通常是连续排列
    3. 主要用于展示数值型数据

绘图时x轴/y轴的刻度标签重叠

Matplotlib绘图时x轴标签重叠的解决办法 - 云+社区 - 腾讯云

  • 方法一:

    拉长画布

  • 方法二:

    调整刻度标签的字体大小:plt.tick_params(axis='x', labelsize=8)

  • 方法三:

    将纵向图改为横向,如条形图:plt.barh()

  • 方法四

    旋转刻度标签的角度plt.xticks(rotation=30)


Pandas常用图表简介

箱线图(箱型图/盒型图)

直方图用于显示一组数据的分布情况,而箱线图用于显示一组数据内部的分散情况。例如: 最大值、最小值,中位数,4分位数、异常值等等。

  • 箱线图各部位的含义

    箱线图各部位的含义

    • 上四分位数(QU):第75%位数

    • 下四分位数(QL):第25%位数

    • 异常值的标准:

      • IQR(Inter-Quatile range):四分位距

        IQR = QU - QLIQR包含了全部观察值的一半

      • 过小的异常值为 $<$ QL-1.5IQR的值

      • 过大的异常值 $>$ QU+1.5IQR的值

    Matplotlib中绘制箱线图的函数为DataFrame.boxplot()


一文学会matplotlib绘图

快捷键

快捷键 功能
Ctrl + R 运行查询语句
Ctrl + F 查找,并有替换选项可勾选

练习一

准备数据

建表语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CREATE TABLE students
(sno VARCHAR(3) NOT NULL,
sname VARCHAR(4) NOT NULL,
ssex VARCHAR(2) NOT NULL,
sbirthday DATETIME,
class VARCHAR(5));

CREATE TABLE courses
(cno VARCHAR(5) NOT NULL,
cname VARCHAR(10) NOT NULL,
tno VARCHAR(10) NOT NULL);

CREATE TABLE scores
(sno VARCHAR(3) NOT NULL,
cno VARCHAR(5) NOT NULL,
degree NUMERIC(10, 1) NOT NULL);

CREATE TABLE teachers
(tno VARCHAR(3) NOT NULL,
tname VARCHAR(4) NOT NULL, tsex VARCHAR(2) NOT NULL,
tbirthday DATETIME NOT NULL, prof VARCHAR(6),
depart VARCHAR(10) NOT NULL);

插入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
INSERT INTO STUDENTS (sno, sname, ssex, sbrithday, class) VALUES (108 ,'曾华' ,'男' ,'1977-09-01',95033);
INSERT INTO STUDENTS (sno, sname, ssex, sbrithday, class) VALUES (105 ,'匡明' ,'男' ,'1975-10-02',95031);
INSERT INTO STUDENTS (sno, sname, ssex, sbrithday, class) VALUES (107 ,'王丽' ,'女' ,'1976-01-23',95033);
INSERT INTO STUDENTS (sno, sname, ssex, sbrithday, classS) VALUES (101 ,'李军' ,'男' ,'1976-02-20',95033);
INSERT INTO STUDENTS (sno, sname, ssex, sbrithday, class) VALUES (109 ,'王芳' ,'女' ,'1975-02-10',95031);
INSERT INTO STUDENTS (sno, sname, ssex, sbrithday, class) VALUES (103 ,'陆君' ,'男' ,'1974-06-03',95031);

INSERT INTO COURSES(cno, cname, tno) VALUES ('3-105' ,'计算机导论',825);
INSERT INTO COURSES(cno, cname, tno) VALUES ('3-245' ,'操作系统' ,804);
INSERT INTO COURSES(cno, cname, tno) VALUES ('6-166' ,'数据电路' ,856);
INSERT INTO COURSES(cno, cname, tno) VALUES ('9-888' ,'高等数学' ,100);

INSERT INTO SCORES(sno, cno, degree) VALUES (103,'3-245',86);
INSERT INTO SCORES(sno, cno, degree) VALUES (105,'3-245',75);
INSERT INTO SCORES(sno, cno, degree) VALUES (109,'3-245',68);
INSERT INTO SCORES(sno, cno, degree) VALUES (103,'3-105',92);
INSERT INTO SCORES(sno, cno, degree) VALUES (105,'3-105',88);
INSERT INTO SCORES(sno, cno, degree) VALUES (109,'3-105',76);
INSERT INTO SCORES(sno, cno, degree) VALUES (101,'3-105',64);
INSERT INTO SCORES(sno, cno, degree) VALUES (107,'3-105',91);
INSERT INTO SCORES(sno, cno, degree) VALUES (108,'3-105',78);
INSERT INTO SCORES(sno, cno, degree) VALUES (101,'6-166',85);
INSERT INTO SCORES(sno, cno, degree) VALUES (107,'6-166',79);
INSERT INTO SCORES(sno, cno, degree) VALUES (108,'6-166',81);

INSERT INTO TEACHERS(tno, tname, tsex, tbirthday, prof, depart) VALUES (804,'李诚','男','1958-12-02','副教授','计算机系');
INSERT INTO TEACHERS(tno, tname, tsex, tbirthday, prof, depart) VALUES (856,'张旭','男','1969-03-12','讲师','电子工程系');
INSERT INTO TEACHERS(tno, tname, tsex, tbirthday, prof, depart) VALUES (825,'王萍','女','1972-05-05','助教','计算机系');
INSERT INTO TEACHERS(tno, tname, tsex, tbirthday, prof, depart) VALUES (831,'刘冰','女','1977-08-14','助教','电子工程系');

练习题

  1. 查询student表中的所有记录的sname、ssex和class列。

    解答:

    1
    2
    SELECT sname, ssex, class
    FROM students;
  2. 查询教师所有的单位,即不重复的depart列。

    解答:

    1
    2
    SELECT DISTINCT depart
    FROM teachers;
  3. 查询students表的所有记录。

    解答:

    1
    2
    SELECT *
    FROM students;
  4. 查询scores表中成绩在60到80之间的所有记录。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 方法一
    SELECT *
    FROM scores
    WHERE degree >= 60 AND degree <= 80;

    # 方法二
    SELECT *
    FROM scores
    WHERE degree BETWEEN 60 AND 80;
  5. ★查询scores表中成绩为85,86或88的记录

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 方法一
    SELECT *
    FROM scores
    WHERE degree IN (85, 86, 88);

    # 方法二
    SELECT *
    FROM scores
    WHERE degree = 85 OR degree = 86 OR degree = 88;
  6. 查询students表中“95031”班性别为“女”的同学记录。

    解答:

    1
    2
    3
    SELECT *
    FROM students
    WHERE class = '95031' OR ssex = '女';
  7. class降序查询students表的所有记录。

    解答:

    1
    2
    3
    SELECT *
    FROM students
    ORDER BY class DESC;
  8. ★以cno升序degree降序查询scores表的所有记录。

    解答:

    1
    2
    3
    SELECT *
    FROM scores
    ORDER BY con, degree DESC;
  9. 查询student表中“95031”班的学生人数

    解答:

    1
    2
    3
    SELECT COUNT(*) AS stu_num
    FROM students
    WHERE class = '95031';
  10. ★★★查询scores表中的最高分的学生学号和课程号。

    解答:子查询

    1
    2
    3
    4
    SELECT sno, cno
    FROM scores
    WHERE degree = (SELECT MAX(degree)
    FROM scores);
  11. ★查询scores表中‘3-105’号课程的平均分

    解答:

    1
    2
    3
    SELECT AVG(degree)
    FROM scores
    WHERE cno = '3-105';
  12. ★★★查询scores表中至少有5名学生选修的并以3开头的课程平均分数

    解答:分组HAVING子句用于过滤分组)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 方法一
    SELECT cno, AVG(degree) AS avg_degree
    FROM scores
    WHERE cno LIKE '3%'
    GROUP BY cno
    HAVING COUNT(*) >= 5;

    # 方法二
    SELECT cno, AVG(degree) AS avg_degree
    FROM scores
    GROUP BY cno
    HAVING COUNT(*) >= 5 AND cno LIKE '3%';
  13. ★★★查询scores表中最低分大于70最高分小于90的sno列。

    解答:

    1
    2
    3
    4
    SELECT sno
    FROM scores
    GROUP BY sno
    HAVING MIN(degree) > 70 AND MAX(degree) < 90;
  14. ★★查询所有学生的sname、cno和degree列。

    解答:

    • 外部联结(OUTER JOIN, OUTER可省略)
    • 内部联结(INNER JOIN)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 方法一:外部联结, LEFT JOIN 左边的表 所有记录都会列出
    SELECT stu.sname, sco.cno, sco.degree
    FROM students AS stu LEFT JOIN scores AS sco
    ON stu.sno = sco.sno
    ORDER BY sname;

    # 方法二:内部联结, INNER JOIN 只返回两个表中联结字段相等的行
    SELECT stu.sname, sco.cno, sco.degree
    FROM students AS stu INNER JOIN scores AS sco
    ON stu.sno = sco.sno
    ORDER BY sname;
  15. ★查询所有学生的sno、cname和degree列。

    解答:外部联结

    1
    2
    3
    4
    SELECT s.sno, c.cname, s.degree
    FROM scores AS s
    LEFT JOIN courses AS c ON s.cno = c.cno
    ORDER BY sno;
  16. ★★★查询所有学生的sname、cname和degree列。

    解答:外部联结

    1
    2
    3
    4
    5
    SELECT st.sname, c.cname, sc.degree
    FROM students AS st
    LEFT JOIN scores AS sc ON st.sno = sc.sno
    LEFT JOIN courses AS c ON sc.cno = c.cno
    ORDER BY sname;
  17. ★★★查询“95033”班所选课程的平均分。

    解答:

    1
    2
    3
    4
    5
    6
    7
    SELECT cname, AVG(degree)
    FROM students AS st
    INNER JOIN scores AS sc ON st.sno = sc.sno
    INNER JOIN courses AS c ON sc.cno = c.cno
    WHERE class = '95033'
    GROUP BY c.cno
    ORDER BY cname;
  18. ★★★假设使用如下命令建立了一个grade表:

    1
    2
    3
    4
    5
    6
    7
    CREATE TABLE grade(low INT(3), upp INT(3), rank CHAR(1));
    INSERT INTO grade VALUES(90,100,'A');
    INSERT INTO grade VALUES(80,89,'B');
    INSERT INTO grade VALUES(70,79,'C');
    INSERT INTO grade VALUES(60,69,'D');
    INSERT INTO grade VALUES(0,59,'E');
    COMMIT;

    现查询所有同学的sno、cno和rank列。

    解答:

    1
    2
    3
    4
    SELECT sno, cno, rank
    FROM scores INNER JOIN grade
    ON scores.degree BETWEEN grade.low AND grade.upp
    ORDER BY sno;
  19. ★★★查询选修“3-105”课程成绩高于“109”号同学成绩的所有同学的记录。

    解答:

    • 自联结
    1
    2
    3
    4
    5
    6
    SELECT sname, s1.degree
    FROM scores AS s1
    INNER JOIN scores AS s2 ON s1.cno = s2.cno AND s1.degree > s2.degree
    INNER JOIN students AS st ON s1.sno = st.sno
    WHERE s1.cno = '3-105' AND s2.sno = '109'
    ORDER BY s1.degree;
  20. ★★★查询scores中选学一门以上课程的同学中分数为非最高分成绩的记录。

    解答:

    • 条件:
      1. 选学一门以上课程
      2. 列出这些同学的所有非最高分成绩
    1
    2
    3
    4
    5
    6
    7
    8
    SELECT scores.sno, cno, degree, max_degree
    FROM scores INNER JOIN
    (SELECT sno, MAX(degree) AS max_degree
    FROM scores
    GROUP BY sno
    HAVING COUNT(*) > 1) AS max
    ON scores.sno = max.sno AND degree < max_degree
    ORDER BY sno;
  21. 查询成绩高于学号为“109”、课程号为“3-105”的成绩的所有记录。

    解答:

    1
    2
    3
    4
    5
    6
    7
    SELECT st.sname, s1.cno, s1.degree
    FROM scores AS s1 INNER JOIN scores AS s2
    ON s1.cno = s2.cno AND s1.degree > s2.degree
    INNER JOIN students AS st
    ON s1.sno = st.sno
    WHERE s1.cno = '3-105' AND s2.sno = '109'
    ORDER BY s1.degree;
  22. ★★★查询和学号为108的同学同年出生的所有学生的sno、sname和sbirthday列。

    解答:函数YEAR(d)

    1
    2
    3
    4
    5
    SELECT s1.sno, s1.sname, s1.sbirthday
    FROM students AS s1 INNER JOIN students AS s2
    ON YEAR(s1.sbirthday) = YEAR(s2.sbirthday)
    WHERE s2.sno = '108'
    ORDER BY sbirthday;
  23. 查询“张旭“教师任课的学生成绩。

    解答:

    1
    2
    3
    4
    5
    6
    7
    SELECT sno, degree
    FROM scores INNER JOIN courses
    ON scores.cno = courses.cno
    INNER JOIN teachers
    ON courses.tno = teachers.tno
    WHERE teachers.tname = '张旭'
    ORDER BY degree;
  24. 查询选修某课程的同学人数多于5人教师姓名

    解答:

    1
    2
    3
    4
    5
    6
    7
    SELECT tname
    FROM scores AS s INNER JOIN courses AS c
    ON s.cno = c.cno
    INNER JOIN teachers AS t
    ON t.tno = c.tno
    GROUP BY c.cno
    HAVING COUNT(c.cno) > 5;
  25. 查询95033班和95031班全体学生的记录

    解答:

    1
    2
    3
    4
    SELECT *
    FROM students
    WHERE class IN ('95033', '95031')
    ORDER BY class;
  26. 查询有85分以上成绩的课程cno。

    解答:DISTINCT关键字

    1
    2
    3
    SELECT DISTINCT c.cno
    FROM scores
    WHERE degree > 85;
  27. 查询出“计算机系“教师所教课程的成绩表。

    解答:

    1
    2
    3
    4
    5
    6
    7
    SELECT t.tname, s.cno, cname, sno, degree
    FROM scores AS s INNER JOIN courses AS c
    ON s.cno = c.cno
    INNER JOIN teachers AS t
    ON t.tno = c.tno
    WHERE t.depart = '计算机系'
    ORDER BY t.tname, cname, degree DESC;
  28. 查询“计算机系”中与“电子工程系“的教师不同职称教师的tname和prof。

    解答:

    1
    2
    3
    4
    5
    6
    SELECT tname, prof
    FROM teachers
    WHERE depart = '计算机系' AND prof NOT IN
    (SELECT prof
    FROM teachers
    WHERE depart = '电子工程系');
  29. 查询选修编号为“3-105”课程且成绩至少高于任意选修编号为“3-245”的同学的成绩的cno、sno和degree。

    解答:

    1
    2
    3
    4
    5
    6
    SELECT cno, sno, degree
    FROM scores
    WHERE cno = '3-105' AND degree > (SELECT MIN(degree)
    FROM scores
    WHERE cno = '3-245')
    ORDER BY degree DESC;
  30. 查询选修编号为”3-105“且成绩高于所有选修编号为”3-245“课程的同学的cno、sno和degree。

    解答:

    1
    2
    3
    4
    5
    6
    SELECT cno, sno, degree
    FROM scores
    WHERE cno = '3-105' AND degree > (SELECT MAX(degree)
    FROM scores
    WHERE cno = '3-245')
    ORDER BY degree DESC;
  31. 查询所有教师和同学的name、sex和birthday。

    解答:组合查询UNION

    • UNION中每个查询必须包含相同的列、表达式或聚集函数(不过各个列不需要以相同的次序列出)。
    • 列数据类型必须兼容。类型不必完全相同,但必须是DBMS可以隐含地转换的类型(如不同的数值类型不同的日期类型)。
    1
    2
    3
    4
    5
    6
    SELECT tname AS name, tsex AS sex, tbirthday AS birthday
    FROM teachers
    UNION
    SELECT sname AS name, ssex AS sex, sbirthday AS birthday
    FROM students
    ORDER BY birthday;
  32. 查询所有“女”教师和“女”同学的name、sex和birthday。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    SELECT tname AS name, tsex AS sex, tbirthday AS birthday
    FROM teachers AS t
    WHERE t.tsex = '女'
    UNION
    SELECT sname AS name, ssex AS sex, sbirthday AS birthday
    FROM students AS s
    WHERE s.ssex = '女'
    ORDER BY birthday;
  33. ★★★查询成绩比该课程平均成绩低的同学的成绩表。

    解答:

    1
    2
    3
    4
    5
    6
    SELECT s1.*, avg_degree
    FROM scores AS s1 INNER JOIN (
    SELECT cno, AVG(degree) AS avg_degree
    FROM scores
    GROUP BY cno) AS s2
    ON s1.cno = s2.cno AND s1.degree < s2.avg_degree;
  34. 查询所有任课教师的tname和depart。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    SELECT DISTINCT tname, depart
    FROM courses AS c INNER JOIN teachers AS t
    ON c.tno = t.tno
    ORDER BY tname;

    SELECT tname, depart
    FROM teachers
    WHERE tno IN(
    SELECT tno
    FROM courses)
    ORDER BY tname;
  35. ★查询所有未讲课的教师的Tname和Depart。

    解答:

    1
    2
    3
    4
    5
    6
    SELECT tname, depart
    FROM teachers
    WHERE tno NOT IN(
    SELECT tno
    FROM courses)
    ORDER BY tname;
  36. ★★查询至少有2名男生的班号。

    解答:

    1
    2
    3
    4
    5
    SELECT class
    FROM students
    WHERE ssex = '男'
    GROUP BY class
    HAVING COUNT(*) >= 2;
  37. ★查询students表中不姓“王”的同学记录。

    解答:

    • LIKE操作符
    • 通配符%
    1
    2
    3
    SELECT *
    FROM students
    WHERE sname NOT LIKE '王%';
  38. ★★查询students表中每个学生的姓名和年龄

    解答:

    • 当前日期函数CURDATE()
    • 当前日期和时间函数NOW()
    1
    2
    SELECT sname, TIMESTAMPDIFF(YEAR, sbirthday, CURDATE()) AS age
    FROM students;
  39. ★★查询students表中最大最小的sbirthday日期值。

    解答:

    1
    2
    SELECT DATE(MAX(Sbirthday)) AS max_birthday, DATE(MIN(Sbirthday)) AS min_birthday,
    FROM Students;
  40. 班号年龄从大到小的顺序查询students表中的全部记录。

    解答:

    1
    2
    3
    SELECT *
    FROM students
    ORDER BY class DESC, TIMESTAMPDIFF(YEAR, sbirthday, CURDATE()) DESC;
  41. 查询“男”教师及其所上的课程

    解答:

    1
    2
    3
    4
    5
    SELECT tname, cname
    FROM teachers AS t INNER JOIN courses AS c
    ON t.tno = c.tno
    WHERE tsex='男'
    ORDER BY tname;
  42. ★查询最高分同学的sno、cno和degree列。

    解答:

    1
    2
    3
    4
    SELECT sno, cno, degree
    FROM scores
    GROUP BY cno
    HAVING degree=MAX(degree);
  43. 查询和“李军”同性别的所有同学的sname。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 方法一 子查询
    SELECT sname
    FROM students
    WHERE ssex = (
    SELECT ssex
    FROM students
    WHERE sname = '李军');

    # 方法二 联结表
    SELECT s1.Sname
    FROM students AS s1 INNER JOIN students AS s2
    ON s1.ssex=s2.ssex
    WHERE s2.sname = '李军';
  44. ★★查询和“李军”同性别同班的同学sname。

    解答:

    1
    2
    3
    4
    SELECT sname
    FROM students AS s1 INNER JOIN students AS s2
    ON s1.ssex = s2.ssex AND s1.class = s2.class
    WHERE s2.sname = '李军';
  45. 查询所有选修“计算机导论”课程“男”同学的成绩表。

    解答:

    1
    2
    3
    4
    5
    6
    7
    SELECT sname, degree
    FROM scores AS s1 INNER JOIN courses AS c
    ON s1.cno = c.cno
    INNER JOIN students AS s2
    ON s1.sno = s2.sno
    WHERE c.cname = '计算机导论' AND s2.ssex = '男'
    ORDER BY degree DESC;

练习二

准备数据

建表语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE userinfo (
userId int(11) NOT NULL,
sex varchar(2),
birth date,
PRIMARY KEY (userId)
) ENGINE = InnoDB;

CREATE TABLE orderinfo (
orderId int(11) NOT NULL,
userId int(11) NOT NULL,
isPaid varchar(10),
price float(11, 2),
paidTime timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0),
PRIMARY KEY (orderId)
) ENGINE = InnoDB;

导入数据

  • 获取链接:用户消费行为分析数据;提取码:yu63

  • 数据文件

    • user_info_utf.csv
    • order_info_utf.csv
  • 通过navicat,选择对应的数据库表,选择导入

    1. 导入类型:CSV File

      • 导入从:选择需要导入的数据文件
      • 编码:UTF-8(默认)
    2. 记录分隔符:CRLF(默认)

      • 字段名行:若数据文件不包含数据表的列名,则输入0(默认为第1行);
      • 第一个数据行:若文件不包含数据表的列名,则输入1(默认为第2行);
      • 日期排序:注意年月日的顺序
    3. 确认目标表

    4. 确认对应的目标字段

    5. 将记录添加到目标表/删除目标表中原数据,用导入的记录代替

    6. 开始执行


练习题

  1. 统计不同月份的下单人数

    1
    2
    3
    4
    SELECT MONTH(paidTime), COUNT(DISTINCT userId)
    FROM orderinfo
    WHERE isPaid = '已支付'
    GROUP BY MONTH(paidTime);
  2. 统计用户三月份复购率和回购率(三月份购买的用户,四月份也购买)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # 复购率
    SELECT COUNT(cnt) AS 下单用户数, COUNT(IF(cnt > 1, 1, NULL)) AS 复购用户数
    FROM (SELECT userId, COUNT(userId) as cnt
    FROM orderinfo
    WHERE isPaid = '已支付' AND MONTH(paidTime) = 3
    GROUP BY userId) AS t;

    # 3月份的回购率
    SELECT t1.m, COUNT(t1.m), COUNT(t2.m)
    FROM (SELECT userId, DATE_FORMAT(paidTime, '%Y-%m-01') AS m
    FROM orderinfo
    WHERE isPaid = '已支付'
    GROUP BY userId, DATE_FORMAT(paidTime, '%Y-%m-01')) AS t1
    LEFT JOIN (SELECT userId, DATE_FORMAT(paidTime, '%Y-%m-01') AS m
    FROM orderinfo
    WHERE isPaid = '已支付'
    GROUP BY userId, DATE_FORMAT(paidTime, '%Y-%m-01')) AS t2
    ON t1.userId = t2.userId AND t1.m = DATE_ADD(t2.m, INTERVAL -1 MONTH)
    GROUP BY t1.m;
  3. 统计男女用户的消费频次(平均数)是否有差异

1
2
3
4
5
6
7
8
9
SELECT sex, AVG(cnt)
FROM (SELECT o.userId, sex, COUNT(*) AS cnt # count(*)的作用为 统计消费次数
FROM orderinfo AS o
INNER JOIN (SELECT *
FROM userinfo
WHERE sex != '') AS t
ON o.userId = t.userId
GROUP BY o.userId, sex) AS t2
GROUP BY sex;
  1. 统计多次消费的用户,第一次和最后一次消费间隔是多少

    1
    2
    3
    4
    5
    SELECT userId, DATEDIFF(MAX(paidTime), MIN(paidTime)) 
    FROM orderinfo
    WHERE isPaid = '已支付'
    GROUP BY userId
    HAVING COUNT(*) > 1;
  2. 统计不同年龄段,用户的消费金额是否有差异?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    SELECT ageGroup, AVG(cnt)
    FROM (SELECT o.userId, ageGroup, count(o.userId) AS cnt
    FROM orderinfo AS o
    INNER JOIN (SELECT userId, CEIL((YEAR(NOW()) - YEAR(birth)) / 10) AS ageGroup
    FROM userinfo
    WHERE birth > '1901-00-00') AS t
    ON o.userId = t.userId
    WHERE isPaid = '已支付'
    GROUP BY o.userId, ageGroup) AS t2
    GROUP BY ageGroup;
  3. 统计消费的二八法则,消费的top20%用户,贡献了多少额度

    1
    2
    3
    4
    5
    6
    7
    SELECT COUNT(userId), SUM(total)
    FROM (SELECT userId, SUM(price) AS total
    FROM orderinfo AS o
    WHERE isPaid = '已支付'
    GROUP BY userId
    ORDER BY total DESC
    LIMIT 17000) AS t;

练习三

准备数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CREATE TABLE datafrog_test1
(userid VARCHAR(20),
changjing VARCHAR(20),
int_time VARCHAR(20)
);

INSERT INTO datafrog_test1
VALUES (1,1001,1400),
(2,1002,1401),
(1,1002,1402),
(1,1001,1402),
(2,1003,1403),
(2,1004,1404),
(3,1003,1400),
(4,1004,1402),
(4,1003,1403),
(4,1001,1403),
(4,1002,1404),
(5,1002,1402),
(5,1002,1403),
(5,1001,1404),
(5,1003,1405);

问题

求用户id对应的前两个不同场景。(场景重复的话,选场景的第一个访问时间,场景不足两个也输出其场景)


解答


Leetcode 上的SQL题



牛客网上的SQL题

  • 获取当前薪水第二多的员工的emp_no以及其对应的薪水salary

  • 不让使用ORDER BY排序时,利用子查询MAX()

  • 查找所有员工自入职以来的薪水涨幅情况

    • 嵌套查询
  • 对所有员工的薪水按照salary进行按照1-N的排名

    • 窗口函数
    • 排序细节
  • 获取所有非manager员工当前的薪水情况

    • 使用INNER JOIN而不是LEFT JOIN
    • 一个部门里可能有多个manager,所以用NOT IN比用!=更合理
  • 获取员工其当前的薪水比其manager当前薪水还高的相关信息

    • 创建两张表(一张记录当前所有员工的工资,另一张只记录部门经理的工资)进行比较
  • 给出每个员工每年薪水涨幅超过5000的员工编号emp_no

    • 题意模糊:应该为给出薪水与去年相比丈夫超过5000的员工编号
    • 使用INNER JOIN而不是LEFT JOIN
    • 时间线的判断方法很有参考价值
  • 查找描述信息中包含robot的电影对应的分类名称以及电影数目,而且还需要该分类对应电影数量>=5部

    • 筛选方法
  • 创建一个actor表,包含如下列信息

    • 创建表的时候,有些地方必须加括号
  • 批量插入数据,不使用replace操作

    • MySQL插入数据,如果数据已存在则忽略的语句是INSERT IGNORE table_name VALUES(...);
  • 对first_name创建唯一索引uniq_idx_firstname

    • 创建索引
  • 针对actor表创建视图actor_name_view

    • 创建视图
  • 针对上面的salaries表emp_no字段创建索引idx_emp_no

    • 强制索引(MySQL 为 FORCE INDEX)
  • 构造一个触发器audit_log

    • 此触发器必须按照AFTER INSERT执行,因为在BEFORE INSERT语句执行之前,新的行数据还没有生成
      • 通常,将BEFORE用于数据验证和净化
  • 删除emp_no重复的记录,只保留最小的id对应的记录

    •   DELETE FROM table_name 
        WHERE xxx
        
      1
      2
      3
      4
      5
      6
      7

      - [将所有to_date为9999-01-01的全部更新为NULL](https://www.nowcoder.com/practice/859f28f43496404886a77600ea68ef59?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)

      - ```sql
      UPDATE table_name
      SET xxx
      WHERE xxx;
  • 将id=5以及emp_no=10001的行数据替换成id=5以及emp_no=10005

    • REPLACE(s, s1, s2)函数
  • 将titles_test表名修改为titles_2017

    • 修改表名的两种方式
      • RENAME TABLE table_name1 TO table_name2;
      • ALTER TABLE table_name1 RENAME TO table_name2;
  • 在audit表上创建外键约束,其emp_no对应employees_test表的主键id

    •   ALTER TABLE table_name
        ADD [CONSTRAINT fk_name] -- CONSTRAINT fk_name 定义外键名,可不定义
        FOREIGN KEY (xx) REFERENCES another_table (xx);
        
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82

      - [将所有获取奖金的员工当前的薪水增加10%](https://www.nowcoder.com/practice/d3b058dcc94147e09352eb76f93b3274?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)

      - MySQL不支持`*=`这样的简化字符使用

      - [使用含有关键字exists查找未分配具体部门的员工的所有信息](https://www.nowcoder.com/practice/c39cbfbd111a4d92b221acec1c7c1484?tpId=82&tags=&title=&diffculty=0&judgeStatus=&rp=1)

      - EXISTS和IN的选择
      - EXISTS的用法

      - [获取有奖金的员工相关信息](https://www.nowcoder.com/practice/5cdbf1dcbe8d4c689020b6b2743820bf?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)

      - CASE语句

      - [统计salary的累计和running_total](https://www.nowcoder.com/practice/58824cd644ea47d7b2b670c506a159a6?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)

      - 窗口函数

      - [对于employees表中,给出奇数行的first_name](https://www.nowcoder.com/practice/e3cf1171f6cc426bac85fd4ffa786594?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)

      - 窗口函数

      - [刷题通过的题目排名](https://www.nowcoder.com/practice/cd2e10a588dc4c1db0407d0bf63394f3?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)

      - 窗口函数
      - 排序

      - [异常的邮件概率](https://www.nowcoder.com/practice/d6dd656483b545159d3aa89b4c26004e?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)

      - COUNT函数非NULL的都会计数

      - [牛客每个人最近的登录日期(三)](https://www.nowcoder.com/practice/16d41af206cd4066a06a3a0aa585ad3d?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)
      - 在执行乘法`*`前,**注意括号的使用**
      - `WHERE XX IN XXX`**可以用于对一个组合的查询**

      - ⭐[牛客每个人最近的登录日期(四)](https://www.nowcoder.com/practice/e524dc7450234395aa21c75303a42b0a?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)
      - 解法一:LEFT JOIN + IFNULL函数
      - 笛卡尔积**如果WHERE不满足,整个所生成的元组就不会显示**,**如果要显示的话,就必须使用JOIN连接**
      - 解法二:窗口函数 + SUM函数

      - ⭐⭐[牛客每个人最近的登录日期(五)](https://www.nowcoder.com/practice/ea0c56cd700344b590182aad03cc61b8?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)

      - ⭐[牛客每个人最近的登录日期(六)](https://www.nowcoder.com/practice/572a027e52804c058e1f8b0c5e8a65b4?tpId=82&tags=&title=&diffculty=0&judgeStatus=0&rp=1)

      ---

      ## 最近登陆时间,增量更新:

      table_1是一个按天分区的 用户日志表,每次一个用户登陆都会新加一行:

      day timestamp uid

      2018-08-01 2018-08-01 11:00:00 1

      2018-08-01 2018-08-01 14:00:00 1



      需要根据table_1新建一个table_2:

      Day uid latest_login_time

      2018-08-01 1 2018-08-01 14:00:00



      每天新增一个昨日的分区的全量用户表,其中latest_login_time是自用户登录起最近一次登录的时间

      ```sql
      CREATE TABLE tabel2 (
      day DATE NOT NULL,
      uid INT NOT NULL,
      `latest login time` TIMESTAMP
      );

      INSERT INTO table2(day,
      uid,
      `latest login time`)
      SELECT day, uid, MAX(`timestamp`)
      FROM table_1
      WHERE DATEDIFF(CURDATE(), day) = 1 -- 昨天
      GROUP BY uid;

累计用户数计算

table a是一个用户注册时间的记录表,一个用户只有一条数据,a表如下:

create_time uid

2018-08-01 14:07:09 111

2018-08-02 14:07:09 134

需要计算8月份累计注册用户数(从8月1日开始累计计算,8月1日为8月1日注册用户数,8月2日为8月1日-2日两天的注册用户数),计算结果格式如下:

时间 累计用户数

2018-08-01 2000000

2018-08-02 2100000

…………….

2018-08-31 10000000

1
2
3
4
SELECT DATE(create_time) AS 时间, 
COUNT(uid) OVER (ORDER BY creat_time) AS 累计用户数
FROM `table a`
WHERE YEAR(create_time) = 2018 AND MONTH(create_time) = 8;

连续访问天数

table a 是一个用户登陆时间记录表,每登陆一次会记录一条记录,a表如下:

log_time uid

2018-07-01 12:00:00 123

2018-07-01 13:00:00 123

2018-07-02 14:08:09 456

需要计算出7月1日登陆的用户,在后续连续登陆7天,14天,30天的人数

计算结果格式如下:

7月1日登陆总用户数 连续登陆7天用户数 连续登陆14天用户数 。。。。

1000 500 200

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SELECT COUNT(table_a.uid) AS '7月1日登录总用户数', COUNT(table_b.uid) AS '连续登录7天的用户数', COUNT(table_c.uid) AS '连续登录14天的用户数'
FROM (SELECT DISTINCT uid
FROM consecutive_task
WHERE DATE(login_time) = '2018-07-01') table_a
LEFT JOIN (SELECT uid, (login_date - rank_num) AS rnk
FROM (SELECT uid, login_date, (ROW_NUMBER() OVER (PARTITION BY uid
ORDER BY login_date)) AS rank_num
FROM (SELECT uid, DATE(login_time) AS login_date
FROM consecutive_task
WHERE DATE(login_time) BETWEEN '2018-07-01' AND '2018-07-07'
GROUP BY uid, login_date) b1 ) b2
GROUP BY uid, rnk
HAVING COUNT(rnk) = 7) table_b
ON table_a.uid = table_b.uid
LEFT JOIN (SELECT uid, (login_date - rank_num) AS rnk
FROM (SELECT uid, login_date, (ROW_NUMBER() OVER (PARTITION BY uid
ORDER BY login_date)) AS rank_num
FROM (SELECT uid, DATE(login_time) AS login_date
FROM consecutive_task
WHERE DATE(login_time) BETWEEN '2018-07-01' AND '2020-07-14'
GROUP BY uid, login_date) c1 ) c2
GROUP BY uid, rnk
HAVING COUNT(rnk) = 14) table_c
ON table_a.uid = table_c.uid;

新增用户的近7日留存率

table1:用户新增表,一个设备首次激活都新加一行:

timestamp,device 【新增的日期,新增的设备】

table2: 是一个按天分区的用户活跃表,每次一个用户登陆都会新加一行:

day,device 【活跃的日期,活跃的设备】

需要计算用户新增用户的留存数,留存率【1-7日】

计算结果格式如下:

新增日期 新增设备数 次日留存数 次日留存率 2日留存数 2日留存率 ….

timestamp 1000 500 50% 450 45%

1
2
3
4
5
6
7
SELECT a.first AS 新增日期, COUNT(DISTINCT a.device) AS 新增设备数, 
COUNT(DISTINCT IF(first - b.day = 1, a.device, NULL)) AS 次日留存数,
CONCAT(ROUND(COUNT(DISTINCT IF(first - b.day = 1, a.device, NULL)) / COUNT(DISTINCT a.device) * 100, 0), '%') AS 次日留存率
FROM (SELECT a.device, DATE(a.timestamp) AS first, b.day
FROM table1 a LEFT JOIN table2 b
ON a.device = b.device) c
GROUP BY a.first; -- 按 新增用户的日期 分组

计算日活用户签到,开宝箱,阅读行为的用户各自占比

table1: 是一个按天分区的用户活跃表,每次一个用户登陆都会新加一行:

day,uid 【活跃的日期,活跃的用户id】

table2:是一个按天分区的用户行为表,每一种行为都会新加一行:

day ,uid,type 【日期,用户id,type类型:1表示签到,2表示开宝箱,3表示阅读】

需要计算,签到占日活的比例,开宝箱占日活的比例,阅读占日活的比例

计算结果格式如下:

日期 活跃用户数 签到占日活的比例 开宝箱占日活的比例 阅读占日活的比例

1
2
3
4
5
6
7
8
SELECT day AS 日期, COUNT(DISTINCT uid) AS 活跃用户数, 
COUNT(IF(type = 1, 1, NULL)) / COUNT(uid) AS 签到占日活的比例,
COUNT(IF(type = 2, 1, NULL)) / COUNT(uid) AS 开宝箱占日活的比例,
COUNT(IF(type = 3, 1, NULL)) / COUNT(uid) AS 阅读占日活的比例
FROM (SELECT a.*, b.type
FROM table_1 AS a LEFT JOIN table_2 AS b
ON a.uid = b.uid AND a.day = b.day) c
GROUP BY day;

练习四

准备数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CREATE TABLE Employee 
(
id INT(20),
name VARCHAR(20),
salary INT(20),
departmentid INT(20)
);

INSERT INTO Employee
VALUES (1,"Joe",70000,1),
(2,"Henry",80000,2),
(3,"Sam",60000,2),
(4,"Max",90000,1);

CREATE TABLE Department
(
id INT(20),
name VARCHAR(20)
);

INSERT INTO Department
VALUES (1,"IT"),
(2,"Sales");

练习题

找出每个部门工资最高的员工

1
2
3
4
SELECT d.name Department, e.name Employee, MAX(Salary) Salary
FROM Employee e LEFT JOIN Department d
ON e.departmentid = d.id
GROUP BY d.id;

准备数据

1
2
3
4
5
6
7
8
9
10
CREATE TABLE customer
(
Id INT(10),
Email VARCHAR(20)
);

INSERT INTO customer
VALUES (1,'a@b.com'),
(2,'c@d.com'),
(3,'a@b.com');

练习题

查找 customer 表中所有重复的电子邮箱

1
2
3
4
SELECT Email
FROM customer
GROUP BY Email
HAVING COUNT(Email)>1;

准备数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CREATE TABLE Customers
(
Id INT(10),
Name VARCHAR(20)
);

INSERT INTO Customers
VALUES (1,'Joe'),
(2,'Henry'),
(3,'Sam'),
(4,'Max');

CREATE TABLE Orders
(
Id INT(10),
CustomerId INT(10)
);

INSERT INTO Orders
VALUES (1,3),
(2,1);

练习题:找出所有从不订购任何东西的客户

1
2
3
4
5
6
7
8
9
10
11
-- 方法一
SELECT name AS customers
FROM customers c LEFT JOIN orders o
ON c.id = o.customerId
WHERE o.id IS NULL;

-- 方法二
SELECT name AS customers
FROM customers
WHERE id NOT IN (SELECT customerid
FROM orders);

准备数据

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE Scores
(
id VARCHAR(20),
score FLOAT(4,2)
);

INSERT INTO scores
VALUES (1,3.5),
(2,3.65),
(3,4.00),
(4,3.85),
(5,4.00),
(6,3.65);

练习题:通过查询实现分数排名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
-- 窗口函数 ROW_NUMBER()
SELECT score, ROW_NUMBER() OVER (ORDER BY score DESC) AS `Rank`
FROM scores
ORDER BY score DESC;

-- ROW_NUMBER() 对应 使用变量 的做法
SELECT score, @curRank=@curRank+1 AS `Rank`
FROM scores, (SELECT @RANK:=0) r
ORDER BY score DESC;

-- RANK() (有并列名次的行,会占用下一名次的位置)对应使用变量的做法
SELECT score, `rank`
FROM (SELECT score, @curRank = IF(@preScore = score, @curRank, @incRank) AS `rank`, -- 同分同排名,不同分下一排名
@incRank:=@incRank+1; -- 作用等同于 ROW_NUMBER()
@preScore:=score
FROM scores, (SELECT @curRank:=0, @preScore:=NULL, @incRank:=1) r
ORDER BY score DESC) s;

-- DENSE_RANK() 对应使用变量的做法

SELECT score, `rank`
FROM (SELECT score, @curRank=IF(@preScore=score, @curRank, @curRank+1) AS `rank`,
@preScore=score
FROM scores, (SELECT @curRank:=0, @preScore:=NULL) r
ORDER BY socre DESC) s;

-- 使用CASE WHEN更简洁
SELECT score, CASE
WHEN score=@preScore THEN @curRank
WHEN @preScore:=score THEN @curRank:=@curRank+1 -- @preScore:=score 赋值语句必为true
END AS `rank`
FROM scores, (SELECT @curRank:=0, @preScore:=NULL) r
ORDER BY score DESC;

准备数据

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE seat 
(
id INT(20),
student VARCHAR(20)
);

INSERT INTO seat
VALUES (1,'Abbot'),
(2,'Doris'),
(3,'Emerson'),
(4,'Green'),
(5,'Jeames');

练习题:座位id 是连续递增的,改变相邻俩学生的座位。
要求:

  • 如果学生人数是奇数,则不需要改变最后一个同学的座位。
1
2
3
4
5
6
7
8
SELECT (CASE
WHEN id % 2 = 1 AND id != cnt THEN id+1 -- 奇数且不是最后一个
WHEN id % 2 = 1 AND id = cnt THEN id
ELSE id - 1 -- 偶数
END) AS id, student
FROM seat, (SELECT COUNT(*) AS cnt -- 用于判断当前是否是最后一个学生(奇数的特殊判断)
FROM seat) t
ORDER BY id;

准备数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DROP TABLE IF EXISTS Employee;
CREATE TABLE Employee
(
Id INT(10),
Name VARCHAR(20),
Salary INT(20),
ManagerId INT(10)
);

INSERT INTO Employee
VALUES (1, 'Joe', 70000, 3),
(2, 'Henry', 80000, 4),
(3, 'Sam', 60000, NULL),
(4, 'Max', 90000, NULL);

练习题:编写一个 SQL 查询,获取收入超过他们经理的员工的姓名。

1
2
3
4
SELECT e1.Name
FROM Employee e1 LEFT JOIN Employee e2
ON e1.ManagerId = e2.Id
WHERE e1.Salary > e2.Salary;

练习五

题目:X 市建了一个新的体育馆,每日人流量信息被记录在这三列信息中:序号 (id)、日期 (visit_date)、 人流量 (people)。
请编写一个查询语句,找出
人流量的高峰期

要求:

  • 高峰期时,至少连续三行记录中的人流量不少于100
  • 每天只有一行记录,日期随着 id 的增加而增加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE stadium 
(
id INT(20),
visit_date DATE,
people INT(20)
);

INSERT INTO stadium
VALUES (1,'2017-01-01',10),
(2,'2017-01-02',109),
(3,'2017-01-03',150),
(4,'2017-01-04',99),
(5,'2017-01-05',145),
(6,'2017-01-06',1455),
(7,'2017-01-07',199),
(8,'2017-01-08',188);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
-- 方法一:朴实的自联结
SELECT DISTINCT t1.*
FROM stadium t1, stadium t2, stadium t3
WHERE (t1.people >= 100 -- 筛选表中 人流量>=100 的行
AND t2.people >= 100
AND t3.people >= 100
AND ( (t1.id - t2.id = 1 -- 错位联结,从t1的id开始连续3天有人流高峰
AND t1.id - t3.id = 2
AND t2.id - t3.id = 1
)
OR (t2.id - t1.id = 1 -- 补充 t1之后只有1天人流高峰,但之前也有连续高峰 的情况
AND t2.id - t3.id = 2
AND t1.id - t3.id = 1)
OR (t3.id - t2.id = 1 -- 补充t1为人流高峰最后一天 的情况
AND t2.id - t1.id = 1
AND t3.id - t1.id = 2)
)
)
ORDER BY t1.id;


-- 方法二:MySQL 8.0 之后开始支持 WITH AS 语句(子查询部分,作用类似于一个视图,但with as 属于一次性的,而且必须和其他sql一起使用才行) WITH子句的查询必须用括号括起来
WITH t1 AS
(SELECT id, visit_date, people,
(id - ROW_NUMBER() OVER (ORDER BY id)) rk
FROM stadium
WHERE people >= 100)

SELECT id, visit_date, people
FROM t1
WHERE rk IN (SELECT rk
FROM t1
GROUP BY rk
HAVING COUNT(*) >= 3);

挑战题

1
2
3
4
5
6
7
8
9
10
11
SELECT 日期, COUNT(DISTINCT a.uid) 活跃用户数, COUNT(DISTINCT IF(day-first=1, a.uid, NULL)) 次日留存用户数,
COUNT(DISTINCT IF(day-first=3), a.uid, NULL) 三日留存用户数,
COUNT(DISTINCT IF(day-first=7), a.uid, NULL) 七日留存用户数,
CONCAT(ROUND(COUNT(DISTINCT IF(day-first=1, a.uid, NULL)) / COUNT(DISTINCT a.uid), 2), '%') 次日留存率, CONCAT(ROUND(COUNT(DISTINCT IF(day-first=3, a.uid, NULL)) / COUNT(DISTINCT a.uid), 2), '%') 三日留存率, CONCAT(ROUND(COUNT(DISTINCT IF(day-first=7, a.uid, NULL)) / COUNT(DISTINCT a.uid), 2), '%') 七日留存率
FROM (SELECT uid, DATE_FORMAT(dayno, '%Y-%m-%d') AS first
FROM act_user_info
WHERE app_name = '相机') a LEFT JOIN (SELECT uid, DATE_FORMAT(dayno, '%Y-%m-%d') AS day
FROM act_user_info
WHERE app_name = '相机') b
ON a.uid = b.uid
GROUP BY first;

PDD笔试题

2. 用户行为分析

表1——用户行为表tracking_log,大概字段有(user_id‘用户编号’,opr_id‘操作编号’,log_time‘操作时间’)

  • 统计每天符合以下条件的用户数A操作之后是B操作,AB操作必须相连
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 使用窗口函数
SELECT DATE(log_time), COUNT(DISTINCT user_id)
FROM (SELECT DATE(log_time), user_id, opr_id,
LEAD(opr_id, 1) OVER() AS pre_opr
FROM tracking_log) table_1
WHERE opr_id='A' AND pre_opr='B'
GROUP BY DATE(log_time);


-- 使用用户变量
SELECT DATE(log_time), COUNT(DISTINCT user_id)
FROM (SELECT DATE(log_time), user_id,
(CASE
WHEN @pre_opr='A' AND opr_id='B' THEN @isJoin:=True AND @pre_opr:=opr_id
ELSE @pre_opr:=opr_id
END) isJoin
FROM tracking_log, (SELECT @pre_opr:=NULL, @isJoin:=FALSE) a) table_1
WHERE isJoin=True
GROUP BY DATE(log_time);

3. 用户新增留存分析

表1——用户登陆表user_log,大概字段有(user_id‘用户编号’,log_time‘登陆时间’)

  • 获取每日新增用户数,以及第2天、第30天的回访比例
1
2
3
4
5
6
7
8
SELECT COUNT(user_id) 新增用户数, 
(COUNT(DISTINCT IF(DATEDIFF(DATE(log_time), 注册日期)=1, u1.user_id, NULL)) / COUNT(user_id)) AS2天回访比例,
(COUNT(DISTINCT IF(DATEDIFF(DATE(log_time), 注册日期)=29, u1.user_id, NULL)) / COUNT(user_id)) AS30天回访比例
FROM (SELECT user_id, DATE(MIN(log_time)) AS 注册日期
FROM user_log
GROUP BY user_id) u1 LEFT JOIN user_log u2
ON u1.user_id = u2.user_id
GROUP BY 注册日期;

4. 贝叶斯公式的应用

数据分析笔试题(1) - 知乎


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
SELECT Partition_date, 
(CASE
WHEN DATEDIFF(t2.Partition_date, t1.Partition_date)=1 THEN COUNT(DISTINCT t1.user_id)
END) AS '流失1天',
(CASE
WHEN DATEDIFF(t2.Partition_date, t1.Partition_date)=2 THEN COUNT(DISTINCT t1.user_id)
END) AS '流失2天',
(CASE
WHEN DATEDIFF(t2.Partition_date, t1.Partition_date)=3 THEN COUNT(DISTINCT t1.user_id)
END) AS '流失3天',
(CASE
WHEN DATEDIFF(t2.Partition_date, t1.Partition_date)>=30 THEN COUNT(DISTINCT t1.user_id)
END) AS '流失30天以上'
FROM (SELECT user_id, Partition_date
FROM user_active
WHERE daily_active_status_map=1) t1 LEFT JOIN (SELECT user_id, Partition_date
FROM usre_active
WHERE daily_active_status_map=1) t2
ON t1.user_id=t2.user_id
WHERE t2.user_id IS NULL
GROUP BY Partition_date
ORDER BY Partition_date DESC;

无法插入中文字符

  • 报错内容:

    1366 - Incorrect string value: ‘\xE6\x9B\xBE\xE5\x8D\x8E’ for column ‘sname’ at row 1

  • 原因

    MySQL默认配置:

    variable_name value
    character_set_database latin1
    character_set_server latin1

    可通过SHOW VARIABLES LIKE '%char%';查看。

  • 解决方法

    1. 修改C:\ProgramData\MySQL\MySQL Server 5.7\my.ini

      • 找到[mysql]

        在下方添加default-character-set=utf8

      • 找到[mysqld]

        在下方添加character-set-server=utf8

      • 找到[client]

        在下方添加default-character-set=utf8

    2. 重启MySQL,重新登录

      • Win + R - services.msc

      • 找到MySQL57(57为版本号),右键选择重新启动

      • Win + R - cmd(或cmder)

        通过mysql -u -root -p命令并输入密码后访问数据库。

      • 通过SHOW VARIABLES LIKE '%char%';查看,字符集变为utf8即成功修改。


  • 报错内容:

    2059 - Authentication plugin ‘caching_sha2_password’ cannot be loaded

  • 原因:

    MySQL8之前的版本中加密规则为mysql_native_password,而在MySQL8以后的加密规则为caching_sha2_password

  • 解决方法:

    在命令行中登录MySQL后,使用命令行:

    ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';

第1章 了解SQL

  • 数据库软件应称为DBMS(数据库管理系统)。
  • 数据库是通过DBMS创建和操纵的容器。
  • 在很大程度上说,数据库究竟是文件还是别的什么东西并不重要,因为你并不直接访问数据库;你使用的是DBMS,它替你访问数据库。
  • 模式(schema): 关于数据库布局及特性的信息。
  • 有时,模式用作数据库的同义词。遗憾的是,模式的含义通常在上下文中并不是很清晰。
  • 主键(primary key)
  • SQL(发音为字母S-Q-L或sequel)是结构化查询语言(Structured Query Language)的缩写。
阅读全文 »

Python和Java一样大小写敏感

变量和简单数据类型

字符串

在Python中,用引号(可以是单引号,也可以是双引号)括起的都是字符串。

使用方法修改字符串的大小写

  • title():

    将字符串中每个单词 首字母改为大写其余字母小写

  • upper()

    将字符串中所有字母改为

  • lower()

    将字符串中所有字母改为


合并(拼接)字符串

Python使用+来合并字符串。


使用方法删除多余的空格

  • strip()

    删除字符串首尾空格

  • lstrip():

    删除字符串开头的空格

  • rstrip()

    删除字符串末尾的空格


将字符串中的特定单词都替换为另一个单词

replace()

例子:

1
2
message = "I really like dogs."
message.replace('dog', 'cat')

Python2中的print语句

例如:print "Hello Python 2.7 world!"

无需将要打印的内容放在括号内。从技术上说,Python 3中的print是一个函数,因此括号必不可少。


数字

整数

  • Python用**表示次方运算

    3**2的结果为9。


浮点数


使用函数str()避免类型错误

str()将非字符串值表示为字符串


Python允许在数字中间以_分隔,提高可读性

JDK7的特性:赋值时,可以用下划线_分割过长的数字(整数&浮点数均可),提高可读性


注释

在Python中,用**井号#**标识注释。


列表简介

列表是什么

列表由一系列按特定顺序排列的元素组成,像一个。可以创建包含字母表中所有字母、数字、所有家庭成员姓名的列表,也可以加入任何东西,其中的元素之间可以没有任何关系

在Python中,用方括号[]表示列表,并用逗号来分隔其中的元素。如:

1
2
bicycles = ['trek', 'cannondale', 'redline', 'specialized']
print(bicycles)

Python会打印列表的内部表示包括方括号

1
['trek', 'cannondale', 'redline', 'specialized']

访问列表元素

bicycles[0],请求获取列表元素时,Python只返回该元素,而不包括方括号和引号,是整洁干净的输出

还可以对任何列表元素调用字符串方法。


索引从0开始

  • Python还为访问最后一个列表元素提供了一种特数语法:将索引指定为-1即可访问最后一个列表元素。这种约定也适用于其他负数索引,如**-2返回倒数第二个**列表元素,以此类推。

    列表为空时,这种访问最后一个元素的方式会导致错误


使用列表中的各个值

可以像使用其他变量一样使用列表中的各个值。


修改、添加和删除元素

修改列表元素

指定列表名和要修改的元素的索引,再指定该元素的新值即可。如:

1
2
motorcycles = ['honda', 'yamaha', 'suzuki']
motorcycles[0] = 'ducati'

在列表中添加元素

  • 在列表末尾添加元素:使用append()方法

    1
    motorcycles.append('ducati')

    append()方法让动态地创建列表易如反掌。

  • 在列表中插入元素:使用insert()方法(需要指定新元素的索引和值)

    1
    motorcycles.insert(0, 'ducati')

从列表中删除元素

  • 使用del语句删除元素

    知道要删除的元素在列表中的位置,可以用del语句。

    1
    del motorcycles[0]

    删除元素后,其余元素索引会相应发生变动。

  • 使用**方法pop()**删除元素

    方法pop()可以删除列表末尾元素,并能让你接着使用它

    1
    popped_motorcycle = motorcycle.pop()
  • 弹出列表中任何位置处的元素

    在方法pop()的括号中指定要删除元素的索引即可。

    first_owned = motorcycles.pop(0)

  • 使用方法remove()根据值删除元素

    不知道要从列表中删除的值所处的位置,只知道要删除元素的值是,可以使用**方法remove()**。

    方法remove()只删除第一个指定的值。如果要删除的值可能在列表中出现多次,就需要使用循环来判断是否删除了所有这样的值


组织列表

使用方法sort()对列表进行永久性排序

  • 无参数:按字母顺序排序
  • 填入参数reverse=True:按字母逆序排序
1
2
3
4
5
6
7
8
9
10
11
cars = ['bmw', 'audi', 'toyota', 'subaru']

# 按字母顺序排序
cars.sort()
# 得到结果cars = ['audi', 'bmw', 'subaru', 'toyota']


# 按字母逆序排序
cars.sort(reverse=True)
print(cars)
# 得到结果cars = ['toyota', 'subaru', 'bmw', 'audi']

并非所有的值都是小写时,按字母顺序排列列表要复杂些。决定排列顺序时,有多种解读大写字母的方式,要指定准确的排列顺序可能比较复杂。但是大多数排序方式都基于本节介绍的知识。


使用函数sorted()对列表进行临时排序

1
print(sorted(cars))

反转列表元素

  • reverse()方法

    注意:与sort(reverse=True)中的reverse作用不同。

1
cars.reverse()

确定列表的长度

  • len()方法
1
len(cars)

使用列表时避免索引错误


错误示例:

1
2
motorcycles = ['honda', 'yamaha', 'suzuki']
print(motorcycles[3])

即类似其他语言的数组越界访问,会引起索引错误


操作列表

遍历整个列表

语句为for xxx in xxx:(注意不要遗漏冒号

例子:

1
2
3
magicians = ['alice', 'david', 'carolina']
for magician in magicians:
print(magician)
  • 编写for循环时,对于用于存储列表中每个值的临时变量,可指定任何名称
  • Python根据缩进来判断代码行与前一个代码行的关系。在for循环后面,没有缩进的代码就不是循环的一部分

避免缩进错误

  • 两行独立的代码语句,如果不小心缩进,Python将会报错。

创建数字列表

使用函数range()

1
2
for value in range(1, 5):
print(value)

打印结果为:

1
2
3
4
1
2
3
4

即输出范围左闭右开

习题:使用一个for循环打印数字1~20()

1
2
for value in range(1, 21):
print(value)

使用range()创建数字列表

  • 要创建数字列表,可使用函数list()range()的结果直接转换成列表。
1
2
numbers = list(range(1,6))
# 得到numbers = [1, 2, 3, 4, 5]
  • 使用range()函数还可以指定步长
1
2
even_numbers = list(range(2, 11, 2))
# 得到numbers = [2, 4, 6, 8, 10]
  • 创建包含(1-10)的平方的数字列表
1
2
3
squares = []
for value in range(1,11):
squares.append(value**2)
  • 对数字列表执行简单的统计计算

    • min()
    • max()
    • sum()
    1
    2
    3
    4
    digits = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
    min(digits)
    max(digits)
    sum(digits)

列表解析

列表解析将for循环和创建新元素的代码合并成一行,并自动附加新元素。

如创建平方数列表可以写成:

1
squares = [value**2 for value in range(1, 11)]

要使用这种语法,首先指定一个描述性的列表名,如squares;然后定义一个表达式,用于生成要存储到列表中的值

在上述例子中,表达式为value**2


使用列表的一部分

Python称列表的部分元素切片


切片

创建切片可指定要使用的第一个元素的索引最后一个元素的索引+1(创建范围左闭右开)。

如:

1
2
players = ['charles', 'martina', 'michael', 'florence', 'eli']
print(players[0:3])
  • 如果没有指定第一个索引,Python将自动从列表开头开始

    player[:4]

  • 让切片终止于列表末尾,也可通过省略终止索引实现。

    player[2:]

  • 如果想输出最后三名队员,可通过切片player[-3:]实现


遍历切片

1
2
for player in players[:3]:
print(player);

复制列表

要复制列表,可以通过同时省略起始索引和终止索引创建一个包含整个列表的切片

例如,假设有一个列表,其中包含你最喜欢的三种食品,而你还想创建另外一个列表,其中包含一位朋友喜欢的所有食物。不过你喜欢的视频,这位朋友都喜欢,因此可以通过复制来创建这个列表:

1
2
3
4
5
my_foods = ['pizza', 'falafel', 'carrot cake']
friend_foods = my_foods[:]

# 如下直接赋值,无法得到两个独立的列表
friend_foods = my_foods

元组

列表可以修改的,非常适合用于存储在程序运行期间可能变化的数据集。但有时候需要创建一系列不可修改的元素,元组可以满足这种需求。

Python将不可变的列表称为元组


定义元组

元组看起来犹如列表,但使用圆括号来标识。

例如有一个大小不应改变的矩形,可将其长宽存储在一个元组中:

1
dimensions = (200, 50)

遍历元组中的所有值

同遍历列表相同


修改元组变量

虽然不能修改元组中的元素,但可以给存储元组的变量赋值。因此如果要修改前述矩形的尺寸,可重新定义整个元组

1
2
3
4
dimensions = (200, 50)

# 重新定义元组
dimensions = (400, 100)

设置代码格式

PEP(Python Enhancement Proposal):Python改进提案

PEP 8是最古老的PEP之一,提供了代码格式设置指南。


缩进

PEP 8建议每级缩进都使用四个空格,既提高可读性,又留下了足够的多级缩进空间。

字处理文档中,大家常常使用 制表符 而不是 空格 来缩进。对于字处理文档来说,这样做的效果很好,但混合使用制表符和空格会让Python解释器感到迷惑。每款文本编辑器都提供了将输入的制表符转换为指定数量的空格的设置。在编写代码时应该使用制表符键,但一定要对编辑器进行设置,使其在文档中插入空格而不是制表符


行长

建议每行不超过80字符,PEP 8还建议注释的行长不超过72字符,因为有些工具为大型项目自动生成文档时,会在每行注释开头添加格式化字符。在大多数编辑器中,都可以设置一个视觉标志——通常是一条垂直参考线,让我们知道不能越过的界限在什么地方。


if 语句

条件测试

值为True或False的表达式被称为条件测试

检查是否相等时,大小写的影响

  • 在Python中检查是否相等时区分大小写,两个大小写不同的值会被视为不相等

  • 如果大小写无关紧要,可将变量的值转换为小写,再进行比较:

    1
    2
    3
    car = 'Audi'
    car.lower() == 'audi'
    # 结果为True

检查多个条件

  • 使用and检查多个条件(Java/C/C++ 中为 &&)
  • 使用or检查多个条件(Java/C/C++ 中为 ||)

检查特定值是否包含在列表中

有时候,执行操作前必须检查列表是否包含特定的值。例如,结束用户的注册过程前,可能需要检查用户名是否已包含在用户名列表中。在地图程序中,可能需要检查用户提交的位置是否包含在已知位置列表中。

要判断特定的值是否已包含在列表中,可使用关键字in

1
2
requested_toppings = ['mushrooms', 'onions', 'pineapple']
print('mushrooms' in requested_toppings)

检查特定值是否不包含在列表中

使用关键字not in。例如,如果有一个列表,其中包含被禁止在论坛上发表评论的用户,就可在允许用户提交评论前检查他是否被禁言:

1
2
3
4
5
banned_users = ['andrew', 'carolina', 'david']
user = 'marie'

if user not in banned_users:
print(user.title() + ", you can post a response if you wish.")

布尔表达式

布尔表达式不过是条件测试的别名

布尔值通常用于记录条件,如游戏是否正在运行,或用户是否可以编辑网站的特定内容:

1
2
game_active = True
can_edit = False

if语句

简单的 if 语句

1
2
if conditional_test:
do something

假设有一个表示某人年龄的变量,而你想知道这个人是否够投票的年龄,可使用如下代码:

1
2
3
age = 19
if age >= 18:
print("You are old enough to vote!")

if - else 语句

1
2
3
4
5
age = 19
if age >= 18:
print("You are old enough to vote!")
else:
print("Sorry, you are too young to vote.")

if - elif - else 结构

在现实世界中,很多情况下需要考虑的情形都超过两个。

1
2
3
4
5
6
7
8
9
10
age = 12

if age < 4:
price = 0
elif age < 18:
price = 5
else:
price = 10

print("Your admission cost is $" + str(price) + ".")

使用if语句处理列表

检查特殊元素

1
2
3
4
5
6
7
8
9
requested_toppings = ['mushrooms'. 'green peppers', 'extra cheese']

for requested_topping in requested_toppings:
if requested_topping == 'green peppers':
print("Sorry, we are out of green peppers right now.")
else:
print("Adding " + requested_topping + '.')

print("\nFinished making your pizza!")

确定列表不是空的

1
2
3
4
5
6
7
8
requested_toppings = []

if requested_toppings:
for requested_topping in requested_toppings:
print("Adding " + requested_topping + '.')
print("\nFinished making your pizza!")
else:
print("Are you sure you want a plain pizza?")
  • if语句将 列表名 用在条件表达式中时,Python将在列表至少包含一个元素时返回True,并在列表为空时返回False

使用多个列表

下列示例定义两个列表,其中第一个列表包含披萨店供应的配料,第二个列表包含顾客点的配料。这次对于顾客要求的每个配料,都检查是否时披萨店供应的配料:

1
2
3
4
5
6
7
8
9
10
avaliable_toppings = ['mushrooms', 'olives', 'green peppers',
'pepperoni', 'pineapple', 'extra cheese']
requested_toppings = ['mushrooms'. 'green peppers', 'extra cheese']

for requested_topping in requested_toppings:
if requested_topping in avaliable_toppings:
print("Adding " + requested_topping + ".")
else:
print("Sorry, we don't have " + requested_topping + ".")
print("\nFinished making your pizza!")

字典

  • 列表:用括号[]标识

  • 元组:用括号()标识(不可修改)

  • 字典:用括号{}标识


使用字典

在Python中,字典是一系列 键-值 对。每个键都与一个值相关联,可以使用键来访问与之相关联的值可以将任何Python对象用作字典中的值

键和值之间冒号:分隔,键-值对之间逗号分隔。如:alien_0 = {'color': 'green', 'points': 5}


访问字典中的值

要获取与键相关联的,可依次指定字典名放在方括号内的键

如:

1
2
alien_0 = {'color': 'green', 'points': 5}
new_points = alien_0['points']

添加 键-值 对

字典是一种动态结构可随时添加 键-值 对。要添加 键-值 对,可依次指定字典名、用方括号括起的键相关联的值

如:

1
2
alien_0['x_position'] = 0
alien_0['y_position'] = 25
  • 键-值 对的排列顺序添加顺序不同。Python只关心 键-值 对之间的关联关系

先创建一个空字典

可先使用一对空的花括号定义一个字典,再分行添加各个 键-值 对。

使用字典存储用户提供的数据或在编写能自动生成大量 键-值 对的代码时,通常都需要先定义一个空字典


修改字典中的值

修改字典中的值,可依次指定字典名、用方括号括起的以及与该键相关联的新值


删除 键-值 对

对于字典中不再需要的信息,可使用del语句将相应的 键-值 对彻底删除。使用del语句时,必须指定字典名要删除的键

如:

1
del alien_0['points']
  • 删除的 键-值 对永远消失

由类似对象组成的字典

字典可以存储一个对象的多种信息,也可以存储众多对象的同一种信息

1
2
3
4
5
6
favorite_languages = {
'jen': 'python',
'sarah': 'c',
'edward': 'ruby',
'phil': 'python',
}

遍历字典

遍历所有的 键-值 对

1
2
3
4
5
6
7
8
user_0 = {
'username': 'efermi'.
'first': 'enrico',
'last': 'fermi',
}
for key, value in user_0.items():
print("\nKey: " + key)
print("Value: " + value)

如例子所示,要编写用于遍历字典的for循环。可声明两个变量用于存储键和值,这两个变量可以使用任何名称。

  • 方法items()返回一个 键-值 对列表
  • 即使遍历字典时,键-值 对的返回顺序与存储顺序不同

遍历字典中的所有键

  • 方法keys()返回一个键列表

    1
    2
    for name in favorite_languages.keys():
    print(name.title())
  • 遍历字典时,会默认遍历所有的。因此for name in favorite_languages.keys():for name in favorite_languages:效果相同

下面遍历一下字典中的名字,但在名字为指定朋友的名字时,打印一条消息,指出其喜欢的语言:

1
2
3
4
5
6
7
friends = ['phil', 'sarah']
for name in favorite_languages.keys():
print(name.title())
if name in friends:
print(" Hi " + name.title() +
", I see your favorite language is " +
favorite_languages[name].title() + "!")
  • 还可以使用keys()确定某个人是否接受了调查:

    1
    2
    if 'erin' not in favorite_languages.keys():
    print("Erin, please take our poll!")
  • 方法keys()并非只能用于遍历,实际上,它返回一个包含字典中所有键的列表。


按顺序遍历字典中的所有键

要以特定的顺序返回元素,一种办法是在for循环中对返回的键进行排序。为此,可以使用函数sorted()(临时排序)来获得按特定顺序排列的键列表的副本

1
2
for name in sorted(favorite_languages.keys()):
print(name.title() + ", thank you for taking the poll.")

遍历字典中的所有值

  • 方法values()返回一个值列表

    这种做法提取字典中所有的值,没有考虑是否重复。为剔除重复项,可使用集合(set)。(在C/C++中,set是一个内部自动递增排序不含重复元素的容器)

    1
    2
    for language in set(favorite_languages.values()):
    print(language.title())

嵌套

在列表中存储字典

字典alien_0包含一个外星人的各种信息,但无法存储第二个外星人的信息。如何管理成群结队的外星人呢?一种办法是创建一个外星人列表,其中每个外星人都是一个字典,包含有关该外星人的各种信息(即字典列表)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建一个用于存储外星人的空列表
aliens = []

# 创建30个绿色的外星人
for alien_number in range(0, 30):
new_alien = {'color': 'green', 'points': 5, 'speed': 'slow'}
aliens.append(new_alien)

# 显示前五个外星人
for alien in aliens[:5]:
print(alien)
print("...")

# 显示创建了多少个外星人
print("Total number of aliens: " + str(len(aliens)))
  • 获取列表长度:函数len(列表名)

在字典中存储列表

有时候,需要将列表存储在字典中。例如,你要如何描述顾客点的披萨呢?如果使用列表,只能存储要添加的披萨配料;但使用字典,还可以包含其他有关披萨的描述。

1
2
3
4
5
6
7
pizza = {
'crust': 'thick',
'toppings': ['mushrooms', 'extra cheese'],
}

for topping in pizza['topping']:
print("\t" + topping)
  • 每当需要在字典中将一个键关联到多个值时,都可以在字典中嵌套一个列表

在本章前面有关喜欢的编程语言的示例中,如果将每个人的回答都存储在一个列表中,被调查者就可以选择多种喜欢的语言。因此,在遍历该字典的for循环中,我们需要再使用一个for循环来遍历与被调查者相关联的语言列表:

1
2
3
4
5
6
7
8
9
10
11
favorite_languages = {
'jen': ['python', 'ruby'],
'sarah': ['c'],
'edward': ['ruby', 'go'],
'phil': ['python', 'haskell'],
}

for name, languages in favorite_languages.items():
print("\n" + name.title() + " 's favorite languages are:")
for language in languages:
print("\t" + language.title())

在字典中存储字典

可以在字典中嵌套字典,但这样做时,代码可能很快复杂起来。


将列表转换为字典

1


用户输入和while循环

函数input()的工作原理

函数input()让程序暂停运行,等待用户输入一些文本获取用户输入后,Python将其存储在一个变量中,Python将用户输入解读为字符串

例如,下面的程序让用户输入一些文本,再将这些文本呈现给用户:

1
2
message = input("Tell me something, and I will repeat it back to you: ")
print(message)
  • 函数input()接受一个参数,即要向用户显示的提示或说明
  • 程序等待用户输入,并在用户按回车键后继续运行

编写清晰的程序

有时候,提示可能超过一行,例如,你可能需要指出获取特定输入的原因。这种情况下,可将提示存储再一个变量中,再将该变量传递给函数input()。

1
2
3
4
prompt = "If you tell us who you are, we can personalize the messages you see."
prompt += "\nwhat is your first name? "

name = input(prompt)

使用int()来获取数值输入

函数int()让Python将输入视为数值

1
2
height = input("How tall are you, in inches? ")
height = int(height)

在Python 2.7 中获取输入

Python 2.7应使用函数raw_input()来提示用户输入,这个函数与Python 3 中的nput()一样,将输入解读为字符串。

Python 2.7 也包含函数input(),但它将用户输入解读为Python代码,并尝试运行它们。因此最好的结果是出现错误,指出Python不明白输入的代码;最糟糕的结果是,将运行原本无意运行的代码。


while循环简介

使用标志

在要求很多条件都满足才继续运行的程序中,可定义一个变量,用于判断真个程序是否处于活动状态,这个变量被称为标志。这样,在while语句中,只需检查一个条件——while的当前值是否为True

1
2
3
4
5
6
7
active = True
while active:
message = input()
if message == 'quit':
active = false
else:
print(message)

使用 break 退出循环

1
2
3
4
5
6
while True:
city = input()
if city == 'quit':
break;
else:
print("xxxxx")

在循环中使用 continue


避免无限循环

如果程序陷入无限循环,可按Ctrl + C,也可关闭显示程序输出的终端窗口。

有些编辑器(如 Sublime Text)内嵌了输出窗口,这可能导致难以结束无限循环,因此不得不关闭编辑器来结束无限循环。


使用 While 循环来处理列表和字典

for循环是一种遍历列表的有效方式,但在for循环中不应修改列表,否则将导致Python难以跟踪其中的元素。要在遍历列表的同时对其进行修改,可使用while循环。将while循环同列表和字典结合起来使用,可收集、存储并组织大量输入,供以后查看和显示。


在列表之间移动元素

假设一个列表,其中包含新注册但还未验证的网站用户;验证这些用户后,如何将他们移到另一个已验证用户列表中呢?一种办法是使用一个while循环,在验证用户的同时将其从未验证用户列表中提取出来,再将其加入到另一个已验证用户列表中

1
2
3
4
5
6
7
8
unconfirmed_users = ['alice', 'brian', 'candace']
confirmed_users = []

# 验证每个用户,直到没有未验证用户为止
# 将每个经过验证的用户都移到已验证用户列表中
while unconfirmed_users:
current_user = unconfirmed_users.pop()
confirmed_user.append(current_user)
  • 方法pop()删除列表末尾用户

删除包含特定值的所有列表元素

在第3章中,我们使用方法remove()删除列表中的特定值(只删除第一个指定的值)。如果要删除列表中所有包含特定值的元素,该怎么办?

1
2
3
4
pets = ['dog', 'cat', 'dog', 'goldfish', 'cat', 'rabbit', 'cat']

while 'cat' in pets:
pets.remove('cat')

函数

现有函数

  • round(number, ndigits=None)
    • number:需要进行四舍五入的数字
    • ndigits: 指定的位数,按此位数进行四舍五入

定义函数

下面是一个打印问候语的简单函数:

1
2
3
4
5
def greet_user():
"""显示简单的问候语"""
print("Hello!")

greet_user()
  • 使用关键字def告诉Python要定义一个函数,定义以冒号结尾
  • 第二行的文本"""显示简单的问候语"""是被称为文档字符串(docstring)注释文档字符串三引号括起,Python用它们来生成有关程序中函数的文档

向函数传递信息

1
2
3
4
5
def greet_user(username):
"""显示简单的问候语"""
print("Hello, " + username.title() + "!")

greet_user('jesse')

传递实参

向函数传递实参的方式很多,可使用位置实参,这要求实参的顺序与形参的顺序相同;也可使用关键字实参,其中每个实参都由形参名和值组成;还可使用列表和字典

  • 关键字实参(顺序无关紧要)

    1
    2
    3
    4
    5
    6
    def describe_pet(animal_type, pet_name):
    """显示宠物的信息"""
    print("\nI have a " + animal_type + ".")
    print("My " + animal_type + "'s name is " + pet_name.title() + ".")

    describe_pet(animal_type='hamster', pet_name = 'harry')

默认值

编写函数时,可为每个形参指定默认值,那么在函数调用中就可以省略相应的实参。

1
2
3
4
5
6
def describe_pet(pet_name, animal_type='dog'):
"""显示宠物的信息"""
print("\nI have a " + animal_type + ".")
print("My " + animal_type + "'s name is " + pet_name.title() + ".")

describe_pet(pet_name='willie')
  • 由于给animal_type 指定了默认值,因此在函数调用中只包含一个实参——宠物的名字。然而,Python依然将这个实参视为位置实参,因此如果函数调用中只包含宠物的名字,这个实参将关联到函数定义中的第一个形参,因此需要将pet_name放在形参列表开头。这样,就能在函数调用中只提供小狗的名字了:

    1
    describe_pet('willie')
  • 被指定默认值的形参,可以通过显式地提供实参忽略默认值


返回值

返回简单值

1
2
3
4
5
6
def get_formatted_name(first_name, last_name):
"""返回整洁的姓名"""
full_name = first_name + ' ' + last_name
return full_name.title()

musician = get_formatted_name('jimi', 'hendrix')

让实参变成可选的

1
2
3
4
5
6
7
def get_formatted_name(first_name, last_name, middle_name=''):
"""返回整洁的姓名"""
if middle_name:
full_name = first_name + ' ' + middle_name + ' ' + last_name
else:
full_name = first_name + ' ' + last_name
return full_name.title()
  • Python将非空字符串解读为True

返回字典

1
2
3
4
5
6
7
8
def build_person(first_name, last_name, age=''):
"""返回一个字典,其中包含有关一个人的信息"""
person = {'first': first_name, 'last': last_name}
if age:
person['age'] = age
return person

musician = build_person('jimi', 'hendrix')

传递列表

假设有一个用户列表,我们要问候其中的每位用户。下面的示例将一个名字列表传递给一个名为greet_users()的函数,这个函数问候列表中的每个人:

1
2
3
4
5
6
7
8
def greet_users(names):
"""向列表中的每位用户都发出简单的问候"""
for name in names:
msg = "Hello, " + name.title() + "!"
print(msg)

usernames = ['hannah', 'ty', 'margot']
greet_users(usernames)

禁止函数修改列表

为了防止函数修改列表,可向函数传递列表的副本而不是原件

将列表的副本传递给函数,可以像下面这样做:

1
function_name(list_name[:])
  • 切片表示法[:]创建列表的副本

虽然像函数传递列表的副本可保留原始列表的内容,但除非有充分的理由需要传递副本否则还是应该将原始列表传递给函数。因为让函数使用现成列表避免花时间和内存创建副本,从而提高效率,在处理大型列表时尤其如此。


传递任意数量的实参

有时候,你预先不知道函数需要接受多少个实参,好在Python允许函数从调用语句中收集任意数量的实参

1
2
3
4
5
6
def make_pizza(*toppings):
"""打印顾客点的所有配料"""
print(toppings)

make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')
  • 形参名*toppings中的星号*让Python创建一个名为toppings的空元组(用圆括号()标识,不可修改),并将收到的所有值都封装到这个元组中。

结合使用位置实参和任意数量实参

如果要让函数接受不同类型的实参,必须在函数定义中将接纳任意数量实参的形参放在最后。Python先匹配位置实参关键字实参,再将余下的实参都收集到最后一个形参中

例如,如果前面的函数还需要一个表示披萨尺寸的实参必须将该形参放在形参*toppings前面

1
2
3
4
5
6
7
8
9
def make_pizza(size, *toppings):
"""概述要制作的披萨"""
print("\nMaking a " + str(size) +
"-inch pizza with the following toppings:")
for topping in toppings:
print("- " + topping)

make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')

使用任意数量的关键字实参

有时候,需要接受任意数量的实参,但预先不知道传递给函数的会是什么样的信息。可将函数编写成能够接受任意数量的 键-值 对——调用语句提供了多少就接受多少。

一个示例是创建用户简介:你知道将受到有关用户的信息,但不确定是什么样的信息。

1
2
3
4
5
6
7
8
9
10
11
12
def build_profile(first, last, **user_info):
"""创建一个字典,其中包含我们知道的有关用户的一切"""
profile = {}
profile['first_name'] = first
profile['last_name'] = last
for key, vaule in user_info.items():
profile[key] = value
return profile

user_profile = build_profile('albert', 'einstein',
location='princeton',
field='physics')
  • 形参名**user_info中的两个星号**让Python创建一个名为user_info的空字典(用花括号{}标识),并将收到的所有 名称-值 对都封装到这个字典中。

将函数存储在模块中

使用函数的优点之一是,使用它们可将代码块与主程序分离。通过给函数指定描述性名称,可让主程序容易理解得多。还可以更进一步,将函数存储在被称为模块的独立文件中再将模块导入到主程序中import语句允许在当前运行的程序文件中使用模块中的代码

导入整个模块

要让函数是可导入的,得先创建模块。模块是扩展名为.py的文件包含要导入到程序中的代码

下面来创建一个包含函数make_pizza()的模块。为此,我们将文件pizza.py中除函数make_pizza()之外的其他代码都删除:

1
2
3
4
5
6
def make_pizza(size, *toppings):
"""概述要制作的披萨"""
print("\nMaking a " + str(size) +
"-inch pizza with the following toppings:")
for topping in toppings:
print("- " + topping)

接下来,我们在pizza.py所在的目录中创建另一个名为making_pizzas.py的文件,这个文件导入到刚创建的模块,再调用make_pizza()两次:

1
2
3
4
import pizza

pizza.make_pizza(16, 'pepperoni')
pizza.make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
  • Python读取这个文件时,代码行import pizza让Python打开pizza.py,并将其中的所有函数都复制到这个程序中

  • 要调用被导入的模块中的函数,可指定导入的模块的名称pizza和函数名make_pizza(),并用句点分隔它们。如果你使用这种import语句导入了名为module_name.py的整个模块,就可使用下面的语法来使用其中任何一个函数:

    1
    module_name.function_name()

导入特定的函数

1
from module_name import function_0, function_1, function_2
  • 通过逗号分隔函数名,可根据需要从模块中导入任意数量的函数
  • 使用这种语法,调用函数时就无需使用句点

使用 as 给函数指定别名

要给函数指定别名,需要在导入时这样做。

1
from module_name import function_name as fn
  • 上面的语句将函数function_name()重命名为fn()

使用 as 给模块指定别名

1
import module_name as mn

导入模块中的所有函数

使用星号*运算符可让Python导入模块中的所有函数

1
2
3
from pizza import *

make_pizza(16 'pepperoni')

import语句中的星号让Python将模块pizza中的每个函数都复制到这个文件中。由于导入了每个函数,可通过名称来调用每个函数,而无需使用句点表示法。然而,使用并非自己编写的大型模块时,最好不要采用这种导入方法:Python可能遇到多个名称相同的函数或变量,进而覆盖函数,而不是分别导入所有的函数。

最佳的做法是,要么只导入需要使用的函数,要么导入整个模块并使用句点表示法,这能让代码更清晰,更容易阅读和理解。


函数编写指南

  1. 给函数指定描述性名称,且只使用小写字母下划线

  2. 每个函数都应包含简要地阐述其功能的注释,该注释应紧跟在函数定义后面,并采用文档字符串格式

  3. 给形参指定认值时,等号两边不要有空格

    1
    def function_name(parameter_0, parameter_1='default value')
  4. 对于函数调用中的关键字实参等号两边不要有空格

    1
    function_name(value_0, parameter_1='value')
  5. 如果形参很多,导致函数定义的长度超过了79字符,可在函数定义中输入左括号后按回车键,并在下一行按两次Tab键,从而将形参列表和只缩进一层的函数体区分开来

    1
    2
    3
    4
    def function_name(
    parameter_0, parameter_1, parameter_2,
    parameter_3, parameter_4, parameter_5):
    function body...
  6. 如果程序或模块包含多个函数,可使用两个空行将相邻的函数分开,这样更容易知道前一个函数在什么地方结束,下一个函数从什么地方开始。


高阶函数

  • range() 还可指定步长

  • map()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    def func(x):
    return x*x

    list(map(func, [1, 2, 3, 4, 5]))
    # 效果等价于 [i**2 for i in range(1, 6)]


    a = [1, 2, 3, 4, 5]
    list(map(str, map(func, a)))
    # 结果为 ['1', '4', '9', '16', '25']

匿名函数

使用lambda

1
lambda x:x*x

输入为x,输出x*x

1
2
3
4
5
# 避免显式构造函数func,简约
list(map(lambda x:x*x, [1, 2, 3, 4, 5]))

list(map(lambda x:'char is:'+str(x), [1, 2, 3, 4, 5]))
# 结果为 ['char is:1', 'char is:2', 'char is:3' ...]

第三方包

  • collections
1
2
3
4
5
import collections
a = [1, 2, 3, 1, 2, 31, 2, 1]
print(collections.Counter(a))

# 输出:Counter({1: 3, 2: 3, 3: 1, 31: 1})
  • csv
  • datetime
  • math
  • pandas
  • numpy

创建和使用类

创建 Dog 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Dog():
"""一次模拟小狗的简单尝试"""
def __init__(self, name, age):
"""初始化属性name和age"""
self.name = name
self.age = age

def sit(self):
"""模拟小狗被命令蹲下"""
print(self.name.title() + " is now sitting.")

def roll_over(self):
"""模拟小狗被命令时打滚"""
print(self.name.title() + " rolled over!")
  • 根据约定,Python中首字母大写的名称指的是。这个类定义中的括号是空的,因为我们要从空白创建这个类
  1. 方法__init__()

    • 类中的函数称为方法。就目前而言,函数和方法唯一重要的差别是调用方法的方式

    • 上例中的方法__init__()是一个特殊的方法,每当根据Dog类创建新实例时,Python都会自动运行它

    • 在这个方法名称中,开头和末尾各有两个下划线这是一种约定,旨在避免Python默认方法与普通方法发生名称冲突

    • 这个方法的定义中,形参self必不可少,还必须位于其他形参的前面

      为何必须在方法定义中包含形参self呢?因为Python调用这个__init__()方法将自动传入实参self每个与类相关联的方法调用都自动传递实参self,它是一个指向实例本身的引用让实例能够访问类中的属性和方法

      我们创建Dog实例时,Python将调用Dog类的方法__init__()。我们将通过实参向Dog()传递name和age,self会自动传递

    • self为前缀的变量都可供类中的所有方法使用,我们还可以通过类的任何实例来访问这些变量

      self.name = name获取存储在形参name中的值,并将其存储到变量name中,然后该变量被关联到当前创建的实例

      像这样可以通过实例访问的变量称为属性

    • sit()roll_over()方法不需要额外的信息,因此它们只有一个形参self

  2. 在 Python 2.7 中创建类

    在 Python 2.7 中创建类时,需要做细微的修改——在括号内包含单词object:

    1
    class ClassName(object):

根据类创建实例

1
my_dog = Dog('willie', 6)

遇到上述代码时,Python使用实参willie和6调用Dog类中的方法__init__()。方法__init__()并未显式地包含return语句,但Python自动返回一个表示这条小狗的实例。在这里,命名约定很有用:我们通常可以认为首字母大写的名称指的是小写的名称指的是根据类创建的实例

  1. 访问属性

    my_dog.name,在Dog类中引用这个属性使用的是self.name

  2. 调用方法

    1
    2
    my_dog.sit()
    my_dog.roll_over()

使用类和实例

Car 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Car():
"""一次模拟汽车的简单尝试"""

def __init__(self, make, model, year):
"""初始化描述汽车的属性"""
self.make = make
self.model = model
self.year = year

def get_descriptive_name(self):
"""返回整洁的描述性信息"""
long_name = str(self.year) + ' ' + self.make + ' ' + self.model
return long_name.title()

my_new_car = Car('audi', 'a4', 2016)
print(my_new_car.get_descriptive_name())

运行结果:

1
2016 Audi A4

给属性指定默认值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Car():

def __init__(self, make, model, year):
"""初始化描述汽车的属性"""
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0

def get_descriptive_name(self):
--snip--

def read_odometer(self):
"""打印一条指出汽车里程的消息"""
print("This car has " + str(self.odometer_reading) + " miles on it.")

my_new_car = Car('audi', 'a4', 2016)
print(my_new_car.get_descriptive_name())
my_new_car.read_odometer()

修改属性的值

  1. 直接修改属性的值

    1
    my_new_car.odometer_reading = 23
  2. 通过方法修改属性的值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Car():
    --snip--

    def update_odometer(self, mileage):
    """将里程表读数设置为指定的值"""
    self.odometer_reading = mileage

    my_new_car = Car('audi', 'a4', 2016)
    print(my_new_car.get_descriptive_name())

    my_new_car.update_odometer(23)
    my_new_car.read_odometer()

    可对方法update_odometer()进行扩展,禁止任何人将里程表读数往回调

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Car():
    --snip--

    def update_odometer(self, mileage):
    """
    将里程表读数设置为指定的值
    禁止将里程表读数往回调
    """
    if mileage >= self.odometer_reading:
    self.odometer_reading = mileage
    else:
    print("You can't roll back an odometer!")
  3. 通过方法对属性的值进行递增

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Car():
    --snip--

    def update_odometer(self, mileage):
    --snip--

    def increment_odometer(self, miles):
    """将里程表读数增加指定的量"""
    self.odometer_reading+= miles

继承

一个类继承另一个类时,它将自动获得另一个类的所有属性和方法。原有的类称为父类,新类称为子类。子类同时还可以定义自己的属性和方法

子类的方法__init__()

创建子类的实例时,Python首先需要给父类的所有属性赋值,为此,子类的方法__init__()需要父类施以援手

下面来创建一个简单的ElectricCar类版本,它具备Car类的所有功能

1
2
3
4
5
6
7
8
9
class ElectricCar(Car):
"""电动汽车的独特之处"""

def __init__(self, make, model, year):
"""初始化父类的属性"""
super().__init__(make, model, year)

my_tesla = ElectricCar('tesla', 'model s', 2016)
print(my_tesla.get_descriptive_name())
  • 创建子类时,父类必须包含在当前文件中,且位于子类前面
  • 定义子类时,必须在括号内指定父类的名称
  • super()是一个特殊函数,帮助Python将父类和子类关联起来父类也称为超类(superclass),名称super因此而得名。

Python 2.7 中的继承

在Python 2.7 中,继承语法稍有不同。

1
2
3
4
5
6
7
8
# 在 Python 2.7 中创建类时,需要在括号内包含单词object
class Car(object):
def __init__(self, make, model, year):
--snip--

class ElectricCar(Car):
def __init__(self, make, model, year):
super(ElectricCar, self).__init__(make, model, year)
  • 函数super()需要两个实参:子类名和对象self。

给子类定义属性和方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ElectricCar(Car):
"""电动汽车的独特之处"""

def __init__(self, make, model, year):
"""
电动汽车的独特之处
初始化父类的属性,再初始化电动汽车特有的属性
"""
super().__init__(make, model, year)
self.battery_size = 70

def describe_battery(self):
"""打印一条描述电瓶容量的消息"""
print("This car has a " + str(self.battery_size) + "-kwh battery.")

my_tesla = ElectricCar('tesla', 'model s', 2016)
my_tesla.describe_battery()

重写父类的方法

对于父类的方法,只要它不符合子类模拟的实物的行为都可对其进行重写。为此,可在子类中定义一个与要重写的父类方法同名的方法

假设Car类有一个名为fill_gas_tank()的方法,它对全电动汽车来说毫无意义。下面演示一种重写方式:

1
2
3
4
5
6
class ElectricCar(Car):
--snip--

def fill_gas_tank(self):
"""电动汽车没有油箱"""
print("This car doesn't need a gas tank!")

使用继承时,可让子类保留从父类继承而来的精华,并剔除不需要的糟粕。


将实例用作属性

使用代码模拟实物时,可能会发现给类添加的细节越来越多:属性方法清单以及文件越来越长。这种情况下,可能需要将类的一部分作为一个独立的类提取出来,将大型类拆分称多个协同工作的小类。

例如,不断给ElectricCar类添加细节时,我们可能会发现其中包含很多专门针对汽车电瓶的属性和方法。可以将这些属性和方法提取出来,放到另一个名为Battery的类中,并将一个Battery实例用作ElectricCar类的一个属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Car():
--snip--

class Battery():
"""一次模拟电动汽车电瓶的简单尝试"""

def __init__(self, battery_size=70):
"""初始化电瓶的属性"""
self.battery_size = battery_size

def describe_battery(self):
"""打印一条描述电瓶容量的消息"""
print("This car has a " + str(self.battery_size) + "-kwh battery.")

class ElectricCar(Car):
"""电动汽车的独特之处"""

def __init__(self, make, model, year):
"""
初始化父类的属性,再初始化电动汽车特有的属性
"""
super().__init__(make, model, year)
self.battery = Battery()

my_tesla = ElectricCar('tesla', 'model s', 2016)
my_tesla.battery.describe_battery()

这看似做了很多额外的工作,但现在我们想多详细地描述电瓶都可以,且不会导致ElectricCar类混乱不堪


导入类

Python允许将类存储在模块中,然后在主程序中导入所需的模块(模块是扩展名为.py的文件包含要导入到程序中的代码)。

导入单个类

下面是模块car.py,其中只包含Car类的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
"""一个可用于表示汽车的类"""

class Car():
"""一次模仿汽车的简单尝试"""

def __init__(self, make, model, year):
"""初始化描述汽车的属性"""
self.make = make
self.model = model
self.year = year
self.odometer_reading = 0

def get_descriptive_name(self):
"""返回整洁的描述性名称"""
long_name = str(self.year) + ' ' + self.make + ' ' + self.model
return long_name.title()

def read_odometer(self):
"""打印一条消息,指出汽车的里程"""
print("This car has " + str(self.odometer_reading) + " miles on it.")

def update_odometer(self. mileage):
"""
将里程表读数设置为指定的值
拒绝将历程表往回拨
"""
if mileage >= self.odometer_reading:
self.odometer_reading = mileage
else:
print("You can't roll back an odometer!")

def increment_odometer(self, miles):
"""将里程表读数增加指定的量"""
self.odometer_reading += miles

下面创建另一个文件——my_car.py,在其中导入Car类并创建其实例

1
2
3
4
5
6
7
from car import Car

my_new_car = Car('audi', 'a4', 2016)
print(my_new_car.get_descriptive_name())

my_new_car.odometer_reading = 23
my_new_car.reading_odometer()

导入类是一种有效的编程方式,让大部分逻辑存储在独立的文件中使主程序文件变得整洁而易于阅读


在一个模块中存储多个类

虽然同一个模块中的类之间应存在某种相关性,但可根据需要在一个模块中存储任意数量的类。

类Battery和ElectricCar都可以帮助模拟汽车,因此可以将其加入模块car.py中。


从一个模块中导入多个类

1
from car import Car, ElectricCar
  • 从一个模块中导入多个类时,用逗号分隔各个类

导入整个模块

可以导入整个模块,再使用句点表示法访问需要的类。这中导入方法很简单,代码也易于阅读。由于创建类实例的代码都包含模块名,因此不会与当前文件使用的任何名称发生冲突

1
2
3
4
5
import car

my_beetle = car.Car('volkswagen', 'beetle', 2016)

my_tesla = car.ElectricCar('tesla', 'roadster', 2016)

导入模块中的所有类

要导入模块中的每个类,可使用下面的语法:

1
from module_name import *

但是不推荐这种导入方式(同导入模块中的所有元素),原因有二:

  1. 这种导入方式没有明确地指出你使用了模块中的哪些类
  2. 还可能引发名称方面的困惑。如果不小心导入了一个与程序文件中其他东西同名的类,将引发难以诊断的错误

需要从一个模块中导入很多类时,最好导入整个模块,并module_name.class_name语法来访问类


在一个模块中导入另一个模块

有时候,需要将类分散到多个模块中,以免模块太大,或在同一个模块中存储不相关的类。将类存储在多个模块中时,你可能会发现一个模块中的类依赖于另一个模块中的类。这种情况下,可在前一个模块中导入必要的类。

例如,下面将Car类存储在一个模块中,并将ElectricCar和Battery类存储再另一个模块中。我们将第二个模块命名为electric_car.py:

1
2
3
4
5
6
7
8
9
"""一组可用于表示电动汽车的类"""

from car import Car

class Battery():
--snip--

class ElectricCar(Car):
--snip--

现在可以分别从每个模块中导入类,以根据需要创建任何类型的汽车了:

1
2
3
4
5
6
7
8
from car import Car
from electric_car import ElectricCar

my_beetle = Car('volkswagen', 'beetle', 2016)
print(my_beetle.get_descriptive_name())

my_tesla = ElectricCar('tesla', 'roadster', 2016)
print(my_tesla.get_descriptive_name())

Python 标准库

Python标准库是一组模块,安装好的Python都包含它。可使用标准库中的任何函数和类,为此只需在程序开头包含一条简单的import语句

下面来看模块collection中的一个类——OrderedDict。要创建字典并记录其中的键-值对的添加顺序,即可使用模块collections中的OrderedDict类。再来看一看第6章的favorite_languages.py示例:

1
2
3
4
5
6
7
8
9
10
11
12
from collections import OrderedDict

favorite_languages = OrderedDict()

favorite_languages['jen'] = 'python'
favorite_languages['sarah'] = 'c'
favorite_languages['edward'] = 'ruby'
favorite_languages['phil'] = 'python'

for name, language in favorite_languages.items():
print(name.title() + "'s favorite language is " +
language.title() + ".")

这是一个很不错的类,它兼具列表和字典的主要优点(在将信息关联起来的同时保留原来的顺序)。

模块random

模块random包含以各种方式生成随机数的函数,其中的randint()返回一个位于指定范围内的整数

例如,下面的代码返回一个1~6内的整数:

1
2
from random import randint
x = randint(1, 6)

类编码风格

  • 类名:应采用驼峰命名法
  • 实例名模块名:应采用小写格式,并在单词之间加上下划线。
  • 中,可使用一个空行分隔方法;在模块中,可使用两个空行分隔类
  • 需要同时导入标准库中的模块和自己编写的模块时,先编写导入标准库模块的import语句,再添加一个空行,然后编写导入自己编写的模块的import语句。这种做法让人更容易明白程序使用的各个模块来自何方

文件和异常

从文件中读取数据

文本文件可存储的数据量多的难以置信,每当需要分析或修改存储在文件中的信息时,读取文件都很有用,对数据分析应用程序来说尤其如此。例如可以编写一个这样的程序:读取一个文本文件的内容,重新设置这些数据的格式并将其写入文件,让浏览器能够显示这些内容

要使用文本文件中的信息,首先需要将信息读取到内存中。为此,可以一次性读取文件的全部内容,也可以每次一行逐步读取

读取整个文件

首先创建一个文件,它包含精确到小数点后30位的圆周率值,且在小数点后每10位处都换行

1
2
3
3.1415926535
8979323846
2643383279

将上述文件保存为pi_digits.txt保存到本章程序所在的目录中

下面的程序打开并读取这个文件,再将其内容显示到屏幕上

1
2
3
with open('pi_digits.txt') as file_object:
contents = file_object.read()
print(contents)

再这个程序中,第一行代码做了大量的工作

  • 函数open()

    • 接受一个参数——要打开的文件的名称
    • Python在当前执行的文件所在的目录中查找指定的文件
    • 返回一个表示文件的对象,Python将这个对象存储在我们将在后面使用的变量中。
  • 关键字with不需要访问文件后将其关闭

    • 也可以调用open()close()来打开和关闭文件,但这样做时,如果程序存在bug导致close()语句未执行,文件将不会关闭

      如果在程序中过早调用close(),你会发现需要使用文件时它已关闭(无法访问),这会导致更多的错误

    • 并非在任何情况下都能轻松确定关闭文件的恰当时机,但通过关键字with,可以让Python确定合适的时机自动关闭文件

  • 有了表示文件的对象后,使用方法read()读取这个文件的全部内容作为一个字符串

    • read()到达文件末尾时返回一个空字符串,这个空字符串显示出来就是一个空行
    • 删除末尾的空行,可在print语句中使用rstrip()print(contents.rstrip())

文件路径

Python默认在当前执行的文件所在的目录中查找指定的文件,但有时可能要打开不在程序文件所属目录中的文件。要让Python打开不与程序文件位于同一个目录中的文件,需要提供文件路径

  • 相对文件路径:

    相对于当前运行的程序所在目录的路径。

    • 在 Linux 和 OS X 中,可以这样编写代码:

      with open('text_files/filename.txt') as file_object:

      这行代码让Python到当前文件夹下的text_files文件夹中寻找指定的.txt文件。

    • 在 Windows 系统中,在文件路径中使用反斜杠(\):

      with open('text_files\filename.txt') as file_object:

  • 绝对文件路径:

    将文件在计算机中的准确位置告诉Python。

    绝对路径通常比相对路径更长,因此将其存储在一个变量中,再将该变量传递给open()会有所帮助。

    • 在 Linux 和 OS X 中,可以这样编写代码:

      1
      2
      file_path = '/home/ehmatthes/other_files/text_files/filename.txt'
      with open(file_path) as file_object:
    • 在 Windows 系统中,它们类似于下面这样:

      1
      2
      file_path = 'C:\Ysers\ehmatthes\other_files\text_files\filename.txt'
      with open(file_path) as file_object:

    注意:

    • Windows系统有时能够正确解读文件路径中的斜杠。如果使用Windows系统,且结果不符合预期,请确保在文件路径中使用的是反斜杠
    • 反斜杠在Python中被视为转义标记,为在Windows中确保万无一失,应以原始字符串的方式指定路径,即在开头的单引号前加上r(以r开头,那么说明后面的字符,都是普通的字符了,即如果是\n将表示一个反斜杠字符,一个字母n,而不是表示换行了)。

逐行读取

读取文件时,常常需要检查其中的每一行:你可能要在文件中查找特定的信息,或者要以某种方式修改文件中的文本。例如,你可能要遍历一个包含天气数据的文件,并使用天气描述中包含字样sunny的行;在新闻报道中,你可能会查找包含标签<headline>的行,并按特定的格式设置它。

要以每次一行的方式检查文件,可对文件对象使用for循环

1
2
3
4
5
filename = 'pi_digits.txt'

with open(filename) as file_object:
for line in file_object:
print(line)

我们打印每一行时,发现空白行更多了:

1
2
3
4
5
3.1415926535

8979323846

2643383279

为什么会出现这些空白行呢?因为在这个文件中每行的末尾都有一个看不见的换行符,而print语句也会加上一个换行符。要**消除这些多余的空白行,可在print语句中使用rstrip()**:print(line.rstrip())


创建一个包含文件各行内容的列表

使用关键字with时,open()返回的文件对象只在with代码块内可用。如果要在with代码块外访问文件的内容,可在with代码块内将文件的各行存储在一个列表中,并在with代码块外使用该列表:

1
2
3
4
5
6
7
filename = 'pi_digits.txt'

with open(filename) as file_object:
lines = file_object.readlines()

for line in lines:
print(line.rstrip())
  • 方法readlines()从文件中读取每一行,并将其存储在一个列表中

使用文件的内容

首先创建一个字符串,它包含文件中存储的所有数字,且没有任何空格

1
2
3
4
5
6
7
8
9
10
11
filename = 'pi_digits.txt'

with open(filename) as file_object:
lines = file_object.readlines()

pi_string = ''
for line in lines:
pi_string += line.rstrip()

print(pi_string)
print(len(pi_string))

打印结果:

1
2
3.1415926535  8979323846  2643383279
36

在变量pi_string存储的字符串中,包含原来位于左边的空格,为删除这些空格,**可使用strip()**而不是rstrip()

注意:

读取文本文件时,Python将其中的文本都解读为字符串。如果读取的是数字,并要将其作为数值使用,就必须使用函数int()float()转换为数字。


包含小数点后一百万位的大型文件

只要系统内存足够多,想处理多少数据都可以。


圆周率值中包含你的生日吗

为确认某个人的生日是否包含在圆周率值得前1 000 000位中,可将生日表示为一个由数字组成得字符串,再检查这个字符串是否包含在pi_string中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
filename = 'pi_digits.txt'

with open(filename) as file_object:
lines = file_object.readlines()

pi_string = ''
for line in lines:
pi_string += line.strip()

birthday = input("Enter your birthday, in thje form mmddyy: ")
if birthday in pi_string:
print("Your birthday appears in the first million digits of pi!")
else:
print("Your birthday does not appear in the first million digits of pi.")

写入文件

保存数据最简单的方式之一是将其写入到文件中

写入空文件

要将文本写入文件,在调用open()时需要提供另一个实参,告诉Python要写入打开的文件

1
2
3
4
filename = 'programming.txt'

with open(filename, 'w') as file_object:
file_object.write("I love programming.")
  • 实参'w'告诉Python,我们要以写入模式打开这个文件。

  • 打开文件时,可指定模式

    • 读取模式(**’r’**)
    • 写入模式(**’w’**)
    • 附加模式(**’a’**)
    • 读写模式(**’r+’**)

    如果省略了模式实参,默认以只读模式打开文件。

  • 如果要写入的文件不存在,函数open()自动创建它。然而,以写入(‘w’)模式打开文件时千万要小心,因为如果指定的文件已经存在,Python将在返回文件对象前清空该文件

  • 文件对象的方法write()将一个字符串写入文件

  • Python只能将字符串写入文本文件,要将数值数据存储到文本文件中,必须先使用函数str()将其转换为字符串格式。


写入多行

函数write()不会在写入的文本末尾添加换行符,要让每个字符串都单独占一行,需要在write()语句中包含换行符。


附加到文件

如果要给文件添加内容,而不是覆盖原有内容,可以**附加模式(‘a’)**打开文件。如果指定的文件不存在,Python会创建一个空文件。

1
2
3
4
5
filename = 'programming.txt'

with open(filename, 'a') as file_object:
file_object.write("I also love finding meaning in large datasets.\n")
file_object.write("I love creating apps that can run in a browser.\n")

异常

Python使用被称为异常的特殊对象管理程序执行期间发生的错误。每当发生让Python不知所措的错误时,它都会创建一个异常对象。如果编写了处理该异常的代码,程序将继续运行;如果未对异常进行处理,程序将停止,并显示一个traceback,其中包含有关异常的报告

异常是使用try-except代码块处理的。try-except代码块让Python执行指定的操作同时告诉Python发生异常时怎么办。使用了try-except代码块时,即便出现异常,程序也将继续运行:显示编写的友好的错误消息,而不是令用户迷惑的traceback

  • ZeroDivisionError 异常

    ZeroDivisionError就是一个异常对象

  • ValueError 异常

    尝试将非数字文本转换为数字时,将引发ValueError

  • FileNotFoundError 异常


使用 try-except 代码块

当你认为可能发生了错误时,可编写一个try-except代码块来处理可能引发的异常。

处理ZeroDivisionError异常的try-except代码块类似于下面这样:

1
2
3
4
try:
print(5/0)
except ZeroDivisionError:
print("You can't divide by zero!")

使用异常避免崩溃

发生错误时,如果程序还有工作没有完成妥善地处理错误就尤其重要。这种情况经常会出现在要求用户提供输入的程序中;如果程序能够妥善地处理无效输入就能再提示用户提供有效输入,而不至于崩溃


else 代码块

将可能引发错误地代码放在try-except代码块中,可提高这个程序抵御错误的能力。依赖于try代码块成功执行的代码都应放到else代码块中:

1
2
3
4
5
6
try:
answer = int(first_number) / int(second_number)
except ZeroDivisionError:
print("You can't divide by 0!")
else:
print(answer)

处理 FileNotFoundError 异常

1
2
3
4
5
6
7
8
filename = 'alice.txt'

try:
with open(filename) as f_obj:
contents = f_obj.read()
except FileNotFoundError:
msg = "Sorry, the file " + filename + " does not exist."
print(msg)

如果文件不存在,这个程序什么都不做,因此错误处理代码的意义不大。


分析文本

下面来提取童话Alice in Wonderland的文本,并尝试计算它包含多少个单词

我们将使用方法split(),它根据一个字符串创建一个单词列表。方法split()以空格为分隔符将字符串分拆成多个部分,并将这些部分都存储在一个列表中

1
2
3
4
5
6
7
8
9
10
11
12
13
filename = 'alice.txt'

try:
with open(filename) as f_obj:
contents = f_obj.read()
except FileNotFoundError:
msg = "Sorry, the file " + filename + " does not exist."
print(msg)
else:
# 计算文件大致包含多少个单词
words = contents.split()
num_words = len(words)
print("The file " + filename + " has about " + str(num_words) + " words.")

使用多个文件

下面多分析几本书。我们先将这个程序的大部分代码移到一个名为count_words()的函数中,这样对多本书进行分析时将更容易:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def count_words(filename):
"""计算一个文件大致包含多少个单词"""
try:
with open(filename) as f_obj:
contents = f_obj.read()
except FileNotFoundError:
msg = "Sorry, the file " + filename + " does not exist."
print(msg)
else:
# 计算文件大致包含多少个单词
words = contents.split()
num_words = len(words)
print("The file " + filename + " has about " + str(num_words) + " words.")
filename = 'alice.txt'
count_words(filename)

失败时一声不吭

要让程序在失败时一声不吭,可通过pass语句,在except代码块中明确表明什么都不做

1
2
3
4
5
6
7
8
9
def count_words(filename):
"""计算一个文件大致包含多少个单词"""
try:
--snip--

except FileNotFoundError:
pass
else:
--snip--

pass语句还充当了占位符。它提醒你在程序的某个地方什么都没有做,并且以后也许要在这里做些什么。在这个程序中,我们可能决定将找不到的文件的名称写入到文件missing_files.txt中。


使用 模块json 存储数据

很多程序要求用户输入某种信息,如让用户存储游戏首选项提供可视化的数据。不管专注的是什么,程序都把用户提供的信息存储在列表和字典等数据结构中。用户关闭程序时,几乎总是要保存他们提供的信息,一种简单的方式是使用模块json来存储数据

  • 模块json能将简单的Python数据结构转储到文件中并在程序再次运行时加载该文件中的数据
  • 还可以使用json在Python程序之间分享数据
  • JSON数据格式不是Python专用,因此能够将以JSON格式储的数据与使用其他编程语言的人分享
  • JSON是一种轻便格式,很有用,也易于学习
  • **JSON(JavaScript Object Notation)**格式最初是为JavaScript开发的,但随后成了一种常见格式,被众多语言采用

我们来编写程序,使用json.dump()存储一组数字,使用json.load()将这些数字读取到内存中

使用 json.dump()

  • 函数json.dump()接受两个实参:
    1. 要存储的数据
    2. 可用于存储数据的文件对象
1
2
3
4
5
6
7
import json

numbers = [2, 3, 5, 7, 11, 13]

filename = 'numbers.json'
with open(filename, 'w') as f_obj:
json.dump(numbers, f_obj)
  • 先导入模块json,再创建一个数字列表
  • 通常使用文件扩展名.json指出文件存储的数据为JSON格式
  • 使用函数json.dump()将数字列表存储到文件numbers.json中

使用 json.load()

1
2
3
4
5
6
7
import json

filename = 'numbers.json'
with open(filename) as f_obj:
numbers = json.load(f_obj)

print(numbers)

保存和读取用户生成的数据

来看这样一个例子:用户首次运行程序时被提示输入自己的名字,这样再次运行程序时就记住他了

  • 先存储用户的名字:

    1
    2
    3
    4
    5
    6
    7
    8
    import json

    username = input("What is your name? ")

    filename = 'username.json'
    with open(filename, 'w') as f_obj:
    json.dump(username, f_obj)
    print("We'll remember you when you come back, " + username + "!")
  • 再编写一个程序,向其名字被存储的用户发出问候:

    1
    2
    3
    4
    5
    6
    7
    import json

    filename = 'username.json'

    with open(filename) as f_obj:
    username = json.load(f_obj)
    print("Welcome back, " + username + "!")

我们需要将这两个程序合并到一个程序中。这个程序运行时,我们将尝试从文件username.json中获取用户名。因此首先编写一个尝试恢复用户名的try代码块。如果这个文件不存在,我们就在except代码块中提示用户输入用户名,并将其存储在username.json中,以便程序再次运行时能够获取它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import json

# 如果以前存储了用户名,就加载它
# 否则,就提示用户输入用户名并存储它
filename = 'username.json'
try:
with open(filename) as f_obj"
username = json.load(f_obj)
except FileNotFoundError:
username = input("What is your name? ")
with open(filename, 'w') as f_obj:
json.dump(username, f_obj)
print("We'll remember you when you come back, " + username + "!")
else:
print("Welcome back, " + username + "!")

重构

你经常会遇到这样的情况:代码能够正确地运行,但可做进一步的改进——将代码划分为一系列完成具体工作的函数。这样的过程被称为重构重构让代码更清晰、更易于理解、更容易扩展

要重构上述程序,可将大部分逻辑放到一个或多个函数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import json

def greet_user():
"""问候用户,并指出其名字"""
filename = 'username.json'
try:
with open(filename) as f_obj:
username = json.load(f_obj)
except FileNotFoundError:
username = input("What is your name? ")
with open(filename, 'w') as f_obj:
json.dump(username, f_obj)
print("We'll remember you when you come back" + username + "!")
else:
print("Welcome back, " + username + "!")

greet_user()

下面来重构greet_user(),让它不执行这么多任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import json

def get_stored_username():
"""如果存储了用户名,就获取它"""
filename = 'username.json'
try:
with open(filename) as f_obj:
username = json.load(f_obj)
except FileNotFoundError:
return None
else:
return username

def get_new_username():
"""提示用户输入用户名"""
username = input("What is your name? ")
filename = 'username.json'
with open(filename. 'w') as f_obj:
json.dump(username, f_obj)

def greet_user():
"""问候用户,并指出其名字"""
username = get_stored_username()
if username:
print("Welcom back, " + username + "!")
else:
username = get_new_username()
print("We'll remember you when you come back" + username + "!")

greet_user()

在这个版本中,每个函数都执行单一而清晰的任务


测试代码

测试函数

下面是一个简单的函数,它接受名和姓并返回整洁的姓名:

1
2
3
4
def get_formatted_name(first, last):
"""生成整洁的姓名"""
full_name = first + ' ' + last
return full_name.title()

为核实函数像期望的那样工作,来编写一个使用这个函数的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
from name_function import get_formatted_name

print("Enter 'q' at any time to quit.")
while True:
first = input("\nPlease give me a forst name: ")
if first == 'q':
break
last = input("Please give me a last time: ")
if last == 'q':
break

formatted_name = get_formatted_name(first, last)
print("\tNeatly formatted name: " + formatted_name + '.')

现在假设我们要修改get_formatted_name(),使其还能处理中间名。这样做时,我们要确保不破坏这个函数处理只有名和姓的姓名的方式。为此,我们可以在每次修改get_formatted_name()后都进行测试:运行程序names.py,并输入像Janis Joplin这样的姓名,但这太繁琐了。所幸Python提供了一种自动测试函数输出的高效方式。

单元测试和测试用例

Python标准库中的模块unittest提供了代码测试工具

  • 单元测试:用于核实函数的某个方面没有问题
  • 测试用例:是一组单元测试,这些单元测试一起核实函数在各种情形下的行为都符合要求。

良好的测试用例考虑到了函数可能收到的各种输入,包含针对所有这些情形的测试。全覆盖式测试用例包含一整套单元测试,涵盖了各种可能的函数使用方式。对于大型项目,要实现全覆盖很难。通常,最初只要针对代码的重要行为编写测试即可,等项目被广泛使用时再考虑全覆盖


可通过的测试

创建测试用例的语法需要一段时间才能习惯,但测试用例创建后,再添加针对函数的单元测试就很简单了。要为函数编写测试用例,可先导入模块unittest以及要测试的函数,再创建一个继承unittest.TestCase的类,并编写一系列方法对函数行为的不同方面进行测试。

下面是一个只包含一个方法的测试用例,它检查函数get_formatted_name()在给定名和姓时能否正确地工作:

1
2
3
4
5
6
7
8
9
10
11
12
import unittest
from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):
"""测试name_function.py"""

def test_first_last_name(self):
"""能够正确地处理像Janis Joplin 这样的姓名吗? """
formatted_name = get_formatted_name('janis', 'joplin')
self.assertEqual(formatted_name, 'Janis Joplin')

unittest.main()
  • 代码行unittest.main()让Python运行这个文件中的测试

    1
    2
    3
    4
    5
    .
    ---------------------------------------------------------
    Ran 1 test in 0.000s

    OK
    • 第一行的句点.表明有一个测试通过了
    • 最后的OK表明该测试用例中的所有单元测试都通过了
  • 测试类的命名最好让它看起来与要测试的函数相关,并包含字样Test。这个类必须继承unittest.TestCase类这样Python才知道如何运行你编写的测试

  • 我们运行上述程序时,所有以test_打头的方法都会自动运行

  • unittest最有用的功能之一:一个断言方法。

    **断言方法用来核实得到的结果是否与期望的结果一致(应该满足的条件是否确实满足)**。上述代码通过调用unittest的方法assertEqual(),并向它传递formatted_name'Janis Joplin'


不能通过的测试

测试未通过时结果是什么样的呢?我们来修改get_formatted_name(),使其能够处理中间名,但这样做时,故意让这个函数无法正确地处理像Janis Joplin这样只有名和姓的姓名。

下面是函数get_formatted_name()的新版本,它要求通过一个实参指定中间名

1
2
3
4
def get_formatted_name(first, middle, last):
"""生成整洁的姓名"""
full_name = first + ' ' + middle + ' ' + last
return full_name.title()

这次运行测试代码,将会得到如下输出:

1
2
3
4
5
6
7
8
9
10
11
E
=======================================================
ERROR: test_first_last_name (__main__.NamesTestCase)
------------------------------------------------------
...
...
...
------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)
  • 第一行字母E指出测试用例中有一个单元测试导致了错误
  • 最后一行指出整个测试用例都未通过,因为运行该测试用例时发生了一个错误

添加新测试

我们再编写一个测试,用于测试包含中间名的姓名。为此,在NamesTestCase类中再添加一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import unittest
from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):
"""测试name_function.py"""

def test_first_last_name(self):
"""能够正确地处理像Janis Joplin 这样的姓名吗? """
formatted_name = get_formatted_name('janis', 'joplin')
self.assertEqual(formatted_name, 'Janis Joplin')

def test_first_last_middle_name(self):
"""能够正确地处理像Wolfgang Amadeus Mozart这样的姓名吗? """
formatted_name = get_formatted_name(
'wolfgang', 'mozart', 'amadeus')
self.assertEqual(formatted_name, 'Wolfgang Amadeus Mozart')

unittest.main()
  • 测试方法名必须以test_打头,这样它才会在我们运行test_name_function.py时自动运行。
  • 可以在TestCase类中使用很长的方法名,这些方法名必须是描述性的,这样才能让你明白测试未通过时的输出。

两个测试都通过的输出:

1
2
3
4
5
..
-------------------------------------------------
Ran 2 tests in 0.000s

OK

测试类

各种断言方法

Python在unittest.TestCase类中提供了很多断言方法

6个常用的断言方法:

方法 用途
assertEqual(a, b) 核实a == b
assertNotEqual(a, b) 核实a != b
assertTrue(x) 核实x为True
assertFalse(x) 核实x为False
assertIn(item, list) 核实item在list中
assertNotIn(item, list) 核实item不在list中

一个要测试的类

类的测试与函数的测试相似——所做的大部分工作都是测试类中方法的行为,但存在一些不同之处,下面来编写一个类进行测试。

来看一个帮助管理匿名调查的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class AnonymousSurvey():
"""收集匿名调查问卷的答案"""

def __init__(self, question):
"""存储一个问题,并为存储答案做准备"""
self.question = question
self.responses = []

def show_questinon(self):
"""显示调查问卷"""
print(self.question)

def store_response(self, new_response):
"""存储单份调查答卷"""
self.responses.append(new_response)

def show_results(self):
"""显示收集到的所有答卷"""
print("Survey results:")
for response in self.responses:
print('- ' + response)

为证明AnonymousSurvey类能够正确地工作,我们来编写一个使用它地程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from survey import AnonymousSurvey

# 定义一个问题,并创建一个表示调查地AnonymousSurvey对象
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)

# 显示问题并存储答案
my_survey。show_question()
print("Enter 'q' at any time to quit.\n")
while True:
response = input("Language: ")
if response == 'q':
break
my_survey.store_response(response)

# 显示调查结果
print("\nThank you to everyone who participated in the survey!")
my_survey.show_results()

AnonymousSurvey类可用于进行简单的匿名调查。假设我们将它放在了模块survey中,并想进行改进让每位用户都可输入多个答案;编写一个方法,它只列出不同的答案,并指出每个答案出现了多少次再编写一个类,用于管理非匿名调查

进行上述修改存在风险,可能会影响AnonymousSurvey类的当前行为。要确认在开发这个模块时没有破坏既有行为,可以编写针对这个类的测试。


测试 AnonymousSurvey 类

下面来编写一个测试,对AnonymouSurvey类的行为进行验证:如果用户面对调查问题时只提供了一个答案,这个答案也能被妥善地存储;用户提供三个答案时,也将被妥善地存储:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
"""针对AnonymousSurvey类的测试"""

def test_store_single_response(self):
"""测试单个答案会被妥善地存储"""
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)
my_survey.store_response('English')

self.assertIn('English', my_survey.responses)

def test_store_three_responses(self):
"""测试三个答案会被妥善地存储"""
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)
responses = ['English', 'Spanish', 'Mandarin']
for response in responses:
my_survey.store_response(response)

for response in responses:
self.assertIn(response, my_survey.responses)

unittest.main()

上述做法的效果很好,但这些测试有些重复的地方。下面使用unittest的另一项功能来提高它们的效率。


方法 setUp()

unittest.TestCase类中包含了方法setUp()让我们只需创建这些对象一次,并在每个测试方法中使用它们。如果在TestCase类章包含了方法setUp(),Python将先运行它,再运行各个以test_打头的方法。这样,在每个测试方法中都可使用在方法setUp()中创建的对象了

下面使用setUp()来创建一个调查对象和一组答案,供方法test_store_single_response()test_store_three_responses()使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import unittest
from survey import AnonymousSurvey

class TestAnonymousSurvey(unittest.TestCase):
"""针对AnonymousSurvey类的测试"""

def setUp(self):
"""
创建一个调查对象和一组答案,供使用的测试方法使用
"""
question = "What language did you first learn to speak?"
self.my_survey = AnonymousSurvey(question)
self.responses = ['English', 'Spanish', 'Mandarin']

def test_store_single_response(self):
"""测试单个答案会被妥善地存储"""
self.my_survey.store_response(self.responses[0])
self.assertIn(self.responses[0], self.my_survey.responses)

def test_store_three_responses(self):
"""测试三个答案会被妥善地存储"""
for response in self.responses:
self.my_survey.store_response(response)
for response in self.responses:
self.assertIn(response, self.my_survey.responses)

unittest.main()

方法setUp()做了两件事:

  1. 创建一个调查对象
  2. 创建一个答案列表

存储这两样东西的变量名包含前缀self(即存储在属性中),因此可在这个类的任何地方使用

测试自己编写的类时,方法SetUp()让测试方法编写起来更容易:可setUp()方法中创建一系列实例并设置它们的属性,再在测试方法中直接使用这些实例

注意:

运行测试用例时,每完成一个单元测试,Python都打印一个字符测试通过时打印一个句点;测试引发错误时打印一个E;测试导致断言失败时打印一个F


Python项目

数据可视化

Numpy

Numpy是Python的一种开源的数值计算扩展。可以用来存储和处理大型矩阵,比Python自身的嵌套列表结构(nested list structure)高效得多。在实际工作中直接使用情况较少

阅读全文 »

快捷键

功能 快捷键
编辑单元格内容 【F2】
单元格内换行 Alt】+【Enter】
设置单元格格式 【Ctrl】+【1
Ctrl+E的众多效用
(拼接单元格内容、拆分提取单元格内容、
替换字符、去除空格和符号、自动换行…)
【Ctrl】+【 E】
筛选 【Ctrl】+【Shift】+【L】
输入当前日期 【Ctrl】+【;】
输入当前时间(不含日期) 【Ctrl】+【Shift】+【;】
切换sheet表 【Ctrl】+【PgUp/PgDn】
快速求和 【Alt】+【=】
数据透视表向导 【Alt+D】+【P】
隐藏
取消隐藏
选定目标区域
【Ctrl】 + 【9】
【Ctrl】 + 【Shift】 + 【**(**】
隐藏
取消隐藏
选定目标区域,
【Ctrl】 + 【0】
【Ctrl】 + 【Shift】 +【**)**】
展开或折叠单元格编辑栏 【Ctrl】+【Shift】+【U】
插入
删除
【Ctrl】+【Shift】+【**+**】
【Ctrl】+【-】
设置千位分隔符,并四舍五入为整数 【Ctrl】+【Shift】+【!】
设置百分数形式 【Ctrl】+【Shift】+【%】
设置外边框 【Ctrl】+【Shift】+【&】

绝对引用行/列

使用$符号即可绝对引用


复制粘贴单元格

筛选的情况下

未被筛选的行不会被复制

隐藏的情况下

隐藏的行/列依然会被复制,想要不复制被隐藏的行/列,需要在选中指定区域后,使用【Alt】+【;】快捷键,再进行复制,即可顺利粘贴。


多个单元格中输入相同的数据

  1. 选中所有目标单元格
  2. 输入所需数据,按下【Ctrl】+【Enter】组合键即可。

多个单元格中有部分内容需要重复输入

如:邮箱后缀、电话区号、订单号前缀

希望达到效果:只需向单元格中输入不一样的内容部分,重复部分自动填充

操作:

  1. 选定需要设置的单元格范围,设置单元格格式(Ctrl + 1)

  2. 数字 - 分类 - 自定义 - 类型

  3. 输入框中填写需要的格式

    1. 添加邮箱共同后缀

      输入@"@163.com"

    2. 添加电话区号

      输入"0755-"@

    @用于在数字格式中包含文本,否则文本将不会显示出来


条件格式的设置


批量从身份证号中提取生日

编辑单元格:=TEXT(MID(身份证号所在单元格, 起始位数, 提取长度), "生日格式"(如0000-00-00))


筛选出重复值(唯一值)

  1. 选中需要筛选出重复值(唯一值)的范围
  2. 开始 - 样式 - 条件格式 - 突出显示单元格规则 - 重复值

去除重复值

  1. 选择需要去重的单元格区域
  2. 数据 - 数据工具 - 删除重复值

长数字输入

  • Excel单元格常规型数字的特殊机制
    1. 输入的数字>11位时,自动采用科学计数法
    2. 数字的精度15

最佳解决方式:

设置单元格格式(Ctrl + 1) - 数字 - 分类 - 文本


冻结窗口

  • 冻结指定的某行/列:选中需要冻结的行或列,选择视图 - 窗口 - 冻结窗格即可
  • 冻结指定的多行以及多列
    1. 视图 - 窗口 - 拆分,会显示一个十字型的拆分参考线
    2. 移动拆分参考线至所需的行、列
    3. 视图 - 窗口 - 冻结窗口

粘贴

粘贴值

文件 - 选项 - 快速访问工具栏 - 从下列位置选择命令 - 所有命令 - 粘贴值 - 添加至右侧,之后就可通过【Alt】+ 【提示的另一个键】即可实现快捷键操作。


选中整行/整列 数据

  • 选中整行数据:选定起始单元格Ctrl + Shift + →
  • 选中整列数据:选定起始单元格Ctrl + Shift + ↓

数据透视表

数据透视表(Pivot Table) 是一种交互式的表,可以进行某些计算,如求和与计数等,所进行的计算与数据跟数据透视表中的排列有关。

之所以称为数据透视表,是因为可以动态地改变它们的版面布置以便按照不同方式分析数据,也可以重新安排行号、列标和页字段。每一次改变版面布置时,数据透视表会立即按照新的布置重新计算数据。另外,如果原始数据发生更改,则可以更新数据透视表

  1. 选择需要透视的表格区域
  2. 插入 - 表格 - 数据透视表

函数

VLOOKUP函数

查找,最终返回查询序列中所对应的值。

与之对应的HLOOKUP是按行查找的。

  • 使用场景:
    1. 两张表格
    2. 两张表格中存在相同列
    3. 表2中存在表1不具备的字段,想把表2中的字段关联到表1中
  • 单元格中输入:=VLOOKUP(lookup_value,table_array,col_index_num,range_lookup)
    1. lookup_value:查找的值
    2. table_array:要查找的区域
    3. col_index_num:需要返回的元素在区域中的第几
    4. range_lookup:精确匹配/近似匹配
      • 1/TRUE近似匹配
      • 0/FALSE精确匹配

INDEX + MATCH 函数

  • 使用场景:

    功能与VLOOKUP函数基本相同,但是能弥补VLOOKUP函数的局限性(想要关联的列在相同列的左侧时,无法匹配)。

  • MATCH(lookup_value, lookup_array, [match_type])

    1. lookup_value:查找的值
    2. lookup_array:查找区域(只能1列宽)
    3. [match_type]:
      • 1:小于
      • 0:精确匹配
      • -1:大于

    结果返回查找区域中的所在行数

  • INDEX(array, row_num, [column_num])

    1. array:查找区域
    2. row_num:所在行数(通过MATCH函数获取)
    3. [column_num]:所在列数

AND 函数 / OR 函数

  • AND(logical1,[logical2],...)
  • OR(logical1,[logical2],...)

用于条件判断


IF 函数

IF(logical_test, [value_if_true], [value_if_false])

  1. logical_test:逻辑判断
  2. [value_if_true]:结果为true时的值(字符串使用双引号标注)
  3. [value_if_false]:结果为false时的值

例子:使用IF函数,判断业绩完成情况

单元格中输入:=IF(N4-M4>=0,"已完成", M4-N4)

IF 函数嵌套 示例

=IF(AND(O4="已完成",R4="已完成"),"已完成",IF(AND(O4="已完成",R4<>"已完成"),"仅业绩完成",IF(AND(O4<>"已完成",R4="已完成"),"仅入会完成","均未完成")))


COUNTIFS 函数

COUNTIFS(criteria_range1, criteria1,...)

  • 使用场景:

    对区域中符合条件的单元格进行筛选计数

  • 参数:

    • criteria_range1: 筛选区域

    • criteria1:筛选条件

    • criteria_range2


SUMIFS 函数

SUMIFS(sum_range, criteria_range1, criteria1,...)

  • 使用场景:

    对区域中符合条件的单元格进行筛选求和

  • 参数:

    • sum_range:求和区域

    • criteria_range1:筛选区域

    • criteria1:筛选条件

    • criteria_range2

    求和区域和筛选区域,类似VLOOKUP函数中两张表的感觉


LEFT、MID、RIGHT 函数

  • LEFT(text, [num_chars])
  • MID(text, start_num, num_chars)
  • RIGHT(text, [num_chars])
    • text:文本所在单元格
    • start_num:起始位置是第几个字符
    • num_chars:截取的字符长度

参考

数据分析中常用的9个Excel函数 - 简书

根据《算法笔记》一书,对PAT甲级、乙级真题和codeup部分试题进行分类。

1. C/C++快速入门

1.1 数组


1.2 指针


1.3 结构体的使用


2. 入门模拟

2.1 简单模拟

不涉及算法,只是根据题目描述来编写代码,考察代码能力。

题目 解题关键
codeup 问题 B: A+B \
B1001 害死人不偿命的(3n+1)猜想 \
B1010 一元多项式求导 \
B1011 A+B和C \
B1012 数字分类 有条不紊地整理
B1016 部分A+B \
B1018 锤子剪刀布 注意scanf留在缓冲区的换行符\n的处理
B1026 程序运行时间 \
B1032 挖掘机技术哪家强 统计各学校总分的方式
B1046 划拳 \
A1002 A+B for Polynomials 非零系数项个数的统计
A1009 Product of Polynomials 第二个多项式可边读边处理
A1042 Shuffling Machine 将扑克顺序号转为实际牌号的方式
A1046 Shortest Distance 便于计算距离的方式
A1065 A+B and C (64bit) B1011 A+B 和 C 进阶版
负数相加若溢出,可能得到0

2.2 查找元素

查找是学习写代码的一项基本功。

  • 一般来说,如果需要在一个比较小范围的数据集内内进行查找,直接遍历即可。
  • 如果需要查找的范围较大,可以用二分查找等算法进行更快速的查找
题目 解题关键
B1004 成绩排名 \
B1028 人口普查 1. 不合理年龄的判断方式
2. 需要考虑全不合理的情况
B1032 挖掘机技术哪家强 统计各学校总分的方式
B1041 考试座位号 \
A1011 World Cup Betting \
A1006 Sign In and Sign Out 方法类似 B1028 人口普查
A1036 Boys vs Girls \

2.3 图形输出

做法一般有两种:

  • 通过规律,直接进行输出
  • 定义一个二维字符数组,通过规律填充,然后输出整个二维数组
题目 解题关键
B1027 打印沙漏 沙漏高度和符号数量的关系
B1036 跟奥巴马一起编程 \
A1031 Hello World for U 根据 n1和n3为 $\leq$ n2,且满足 n1+n2+n3 = N+2 的最大值,求出n1,n2,n3

2.4 日期处理

需要处理好平年和闰年、大月和小月的问题,需要细心处理。

闰年的判断方法:

  1. 非整百年:能被4整除为闰年

  2. 整百年:能被400整除的是闰年(1900年不是闰年


2.5 进制转换

对一个P进制的数,如果要转换为Q进制,需要分两步:

  1. 将 P进制数x 转换为 十进制数y

    1
    2
    3
    4
    5
    6
    int y = 0, pro = 1;
    while(x != 0) {
    y += (x % 10) * pro;
    x /= 10;
    pro *= p;
    }
  2. 将 十进制数y 转换为 Q进制数z

    采用除基取余法

    1
    2
    3
    4
    5
    int z[40], num = 0; //数组z 存放 Q进制数y 的每一位,num为位数
    do {
    z[num++] = y % Q; //除基取余
    y /= Q;
    } while(y != 0);

    使用do···while语句而不是while的原因是:如果十进制数y = 0,使用while语句将使循环直接跳出,导致结果出错

题目 解题关键
B1022 D进制的A+B 除基取余法
B1037 在霍格沃茨找零钱 \
A1019 General Palindromic Number \
A1027 Colors in Mars \
A1058 A+B in Hogwarts 题型同 乙级1037 在霍格沃茨找零钱
单位转换过程可能会超过int范围

2.6 字符串处理

考察代码能力的题型。一般需要仔细分析清楚题目中的输入和输出格式才能顺利AC。

有些题目中,可能实现逻辑非常麻烦,有很多细节边界情况,此类题目需要多做多想,积累经验

题目 解题关键
B1002 写出这个数 数字转为字符串
C语言:sprintf(str, "%d", num)
C++:to_string(num)
B1006 换个格式输出整数 \
B1009 说反话 句子颠倒,单词本身不颠倒
C++ 待更新
B1014 福尔摩斯的约会
A1061 Dating
正确归纳解码方式
B1021 个位数统计 \
B1024 科学计数法
A1073 Scientific Notation
1. 利用正则表达式,分开读取 数字部分 和 指数部分
2. 指数 < 0:整数部分必然为 0
3. 指数 >= 0:
- 仍有小数点,何时输出小数点
- 没有小数点,后续输出0
B1031 查验身份证 \
B1048 数字加密 1. 对齐两个整数
- 若加密正整数A 比 B 长,B高位补0后进行加密
- 若加密正整数A 比 B 短,B多余的部分正常输出,等同于A高位补0后进行加密
2. 结果从数字高位(字符串低位)开始输出
A1001 A+B Format 数字高位(字符串低位)开始,需要添加,的位置满足**(i + 1) % 3 == len % 3 且 不是最后一位**
A1005 Spell It Right \
A1035 Password (待优化)
A1077 Kuchiguse 1. 通过reverse()反转字符串,将后缀转换为前缀,便于比较
2. getline()之前注意读取换行符
A1082 Read Number in Chinese 1. 四位数字分为一节,单位为个、万、亿
2. 一节中数字全为0,则不输出节的单位
3. 节中第一个非零数之前有0,则输出1个0

3. 算法初步

3.1 排序

3.1.1 简单选择排序($O(n^2)$)

简单选择排序是指,对一个序列A中的元素A[0] ~ A[n-1],令i从 0 到 n-1 枚举,进行 n 趟操作,每一趟从待排部分$[i, n-1]$中选择最小元素(记录下标),令其与待排部分的**第一个元素A[i]**进行交换,使得$[0, i]$区间有序

1
2
3
4
5
6
7
8
9
10
11
12
void selectSort(int A[]) { //降序 简单选择排序
for(int i = 0; i < n; i++) {
int k = i;
for(int j = i; j < n; j++) { //选出最小的元素
if(A[j] < A[i])
k = j; //记录更小元素的下标
}
int tmp = A[i]; //交换A[k]与A[i]
A[i] = A[j];
A[j] = tmp;
}
}

3.1.2 直接插入排序($O(n^2)$)

直接插入排序是指,对序列A的n个元素A[0]~A[n-1],令 i **从 1 **到 n-1 枚举,进行 n - 1 趟操作。

某一趟时,序列A的前 i 个元素 A[0]~A[i-1]已经有序,则该趟在范围$[0, i - 1]$中,从后往前寻找某个位置j,使得A[i]插入后,范围$[0, i]$有序(A[j]~A[i]后移一位)。

1
2
3
4
5
6
7
8
9
10
11
void insertSort(int A[]) { //升序 直接插入排序
for(int i = 1; i < n; i++) {
int tmp = A[i], j = i;
//从后往前遍历,便于编写代码
while(j > 0 && tmp < A[j - 1]) {
A[j] = A[j - 1];
j--;
}
A[j] = tmp;
}
}

3.1.3 冒泡排序($O(n^2)$)

冒泡排序的本质在于交换,即每次通过交换的方式把当前剩余元素的最大值(升序)移动到一端,当剩余元素减少到0时,排序结束。整个过程执行 n-1 趟,每一趟从左到右依次比较相邻的元素,如果大的数在左边,则交换,该趟结束时,最大数被移动到当前剩余数的最右边

1
2
3
4
5
6
7
8
9
10
11
void bubbleSort(int A[]) { //升序 冒泡排序
for(int i = 1; i < n; i++) {//进行 n - 1 趟排序
for(int j = 0; j < n - i; j++) {
if(a[j] > a[j + 1]) {
int tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
}
}
}
}

3.1.4 排序题与sort函数的应用

PAT中的排序题,大部分只需要得到排序的最终结果,而不需要去写排序的完整过程(例如冒泡排序、快速排序等),因此推荐直接使用C语言的库函数qsort或是C++的sort函数进行排序。qsort函数的使用需要运用指针,且写法上没有sort函数简洁。sort函数根据具体情形使用不同的排序方法,效率较高,在实现中规避了经典快速排序中可能出现的会导致实际复杂度退化到$O(n^2)$的极端情况

因此更推荐使用C++的sort函数进行代码编写。

  • 结构体数组的排序

    1
    2
    3
    4
    struct node {
    int x;
    int y;
    } ssd[10];

    如果想先按x从大到小排序,在x相等的情况下按y从小到大排序(即二级排序),cmp的写法是:

    1
    2
    3
    4
    5
    6
    7
    bool cmp(node a, node b) { //返回值为true时,a排在b之前
    if(a.x != b.x) {
    return a.x > b.x; //降序
    } else {
    return a.y < b.y; //升序
    }
    }
  • 排名的实现

    很多排序题要求在排序之后计算每个个体的排名,规则一般是:分数相同的排名相同,占用一个排位。例如有5个学生的分数分别为90、88、88、88、86,其排名分别为1、2、2、2、5。

    方法:

    将第一个个体排名记为1,遍历剩余个体,如果当前分数等于上一个个体的分数,则当前个体排名等于上一个个体的排名,否则当前个体的排名等于数组下标+1

题目 解题关键
B1015 德才论
A1062 Talent and Virtue
设置flag作为考生的分类,便于所有考生统一排序
A1012 The Best Rank 1. 利用全局变量设计cmp函数
2. 通过记录所有科目的排名,最后选出最好的排名以及对应科目
3. 相同分数者排名相同,下一不同分数者排名为数组下标+1
A1016 Phone Bills (待优化)
1. 通话记录统一先排序后处理
2. 连续的两个通话记录,判断是否为 同一用户 且 为先通话后挂断的状态
3. 通话时长的统计方法
4. 单位美分cents 要转换为 美元$
A1025 PAT Ranking \
A1028 List Sorting \
A1055 The World’s Richest 超时问题。要求输出的人数$\leq$100,通过筛去每个年龄多余的人解决
A1075 PAT Judge (待优化)
1. 不能编译的提交得分为0
2. 没有提交过的答案需要输出为-,利用<cstring>中的memset函数,为 得分数组 赋值 -1,表示没有提交过答案
3. 没有任何一题通过编译 或 没有提交过答案的人不记录排名,设置 是否有通过编译的标识,进行筛选
4. 读取数据时,将用户数组下标看作id,便于统计
5. 排序以 是否有通过编译 为 第一排序条件
A1080
A1083
A1095

3.2 散列

3.2.1 散列(hash)的定义与整数散列

例题:

给出$N$个正整数,再给出$M$个正整数,问这$M$个数中的每个数分别是否在$N$个数中出现过,其中 $N, M \leq10^5$,且所有正整数均不超过$10^5$。例如 $N = 5,M = 3$, $N$个正整数为 ${8, 3, 7, 6, 2}$,欲查询的$M$个正整数为 ${7, 4, 2}$。

对这个问题,最直观的思路是:对每个欲查询的正整数$x$,遍历所有$N$个数,看是否有一个数与$x$相等。这种做法的时间复杂度为$O(NM)$,显然不是好方法。可以采取用空间换时间的方式,用数组下标作为对应整数的标识,即设定一个bool型数组hashTable[100001],其中hashTable[x] == true表示正整数$x$在$N$个整数中出现过。这样就可以在一开始读入$N$个正整数时进行预处理。于是,对$M$个欲查询的数,就能直接通过hashTable数组判断出每个数是否出现过,这种做法的时间复杂度为$O(N+M)$。直接把输入的数作为数组的下标来对这个数的性质进行统计的方法非常实用,务必掌握

但是这个方法有一个问题:如果输入的整数可能是$10^9$大小,甚至是字符串,就不能将其直接作为数组下标了。因此我们寻找一种做法,将元素通过一个函数转换为一个在能接受范围内的整数,使得该整数尽可能唯一地代表这个元素。这样的方法称为散列(hash),用于转换的函数称为散列函数H,也就是说,如果元素在转换前为key,那么转换后就是一个整数H(key)

散列函数

key整数的情况来说,常用的散列函数有:

  1. 直接定址法

    • 恒等变换,即 H(key) = key

    • 线性变换,即 H(key) = a * key + b

  2. 除留余数法

    key除以一个数mod得到的余数作为 hash值 的方法,即 H(key) = key % mod

    通过这个散列函数,可以把很大的数转换为不超过 mod 的整数,这样就可以将它作为可用的数组下标。显然,当mod是一个素数时,H(key)能尽可能覆盖[0,mod]范围内的每一个数

  3. 平方取中法(很少用)

    key平方的中间若干位作为hash值


冲突

当两个不同的数key1key2他们的hash值相同时,这样的情况称为冲突解决冲突的常用方法:

  1. 开放定址法(获取新的hash值)

    • 线性探查法 (Linear Probing)

      当表中下标为H(key)的位置已经被其他元素占用,则检查下一个位置H(key)+1,以此类推。如果检查过程中超过了表长m,那么就回到表的首位继续循环,直到找到一个可用的位置,或是所有位置都已经被使用。

      H = (H(key) + i) % m;**(i = 0, 1, 2, …, m-1)**

      缺点:容易导致大量元素在相邻的散列地址上“聚集”(堆积),大大降低查找效率

    • 平方探查法 (Quadratic Probing)

      当表中下标为H(key)的位置已经被其他元素占用,则按如下顺序检查表中位置

      H = (H(key) + i) % m;i = $0, 1^2, -1^2, 2^2, -2^2, …, k^2, -k^2;(k \leq m/2)$

      避免聚集问题。

      缺点:不能探测到散列表上所有单元(但至少能探测到一半单元)。

    • 再散列法(双散列法)

      H = (H1(key) + i * H2(key)) % mi 是冲突次数

  2. 链地址法 (拉链法)

    不计算新的hash值,而是把 所有H(key)相同的key(称为同义词) 存储在一个线性链表中,由散列地址唯一标识。


一般来说,可以使用标准库模板库中的map来直接使用hash的功能(C++11以后可以用 unordered_map速度更快),因此除非必须模拟这些方法或是对算法的效率要求比较高,一般不需要自己实现上面解决冲突的方法。


3.2.2 字符串hash初步

如果**key不是整数**,应该如何设计散列函数?

例题:

如何将一个二维整点$P$的坐标映射为一个整数,使得整点$P$可以由该整数唯一标识?

假设一个整点$P$的坐标是$(x,y)$,其中$0\leq x,y\leq Range$,那么可以令hash函数为H(P) = x * Range + y,这样对数据范围内的任意两个整点$P1$与$P2$,H(P1)都不会等于H(P2),就可以用H(P)来唯一地标识该整点P,接着便可以通过整数hash的方法进一步映射到较小的范围

字符串hash是指将一个字符串S映射为一个整数,使得该整数可以尽可能唯一地代表字符串S。

为了讨论问题方便,假设字符串均由大写字母A~Z构成,不妨把 AZ 视为 025。接着按照26进制转换为10进制的思路,实现将字符串映射为整数的需求(转换成的整数最大为$26^{len}-1$,len为字符串长度)。

1
2
3
4
5
6
int hashFunc(char S[], int len) { //将字符串S转换为整数
int id = 0;
for(int i = 0; i < len; i++)
id = id * 26 + (S[i] - 'A'); //将26进制转换为10进制
return id;
}

为了避免转换成的整数过大,需要注意 字符串长度len 不能太长。如果字符串中还有小写字母,可以把 AZ 作为 025,把 az 作为 2651,这样就变成了52进制转换为10进制的问题。

1
2
3
4
5
6
7
8
9
10
11
int hashFunc(char S[], int len) {
int id = 0;
for(int i = 0l i < len; i++) {
if(isupper(S[i])) {
id = id * 52 + (S[i] - 'A');
} else if(islower(S[i])) {
id = id * 52 + (S[i] - 'a');
}
}
return id;
}

如果字符中还有数字,一般有两种处理方法:

  1. 按照小写字母的处理方法,增大进制数至62

  2. 如果保证在字符串的末尾是个确定的数字,就可以把前面英文字母的部分按上面的思路转换成整数,再将末尾的数字直接拼接上去

    1
    2
    3
    4
    5
    6
    7
    int hashFunc(char S[], int len) {
    int id = 0;
    for(int i = 0; i < len - 1; i++)
    id = id * 26 + (S[i] - 'A');
    id = id * 10 + (S[len - 1] - '0');
    return id;
    }

例题:

给出N个字符串(由三位大写字母组成),再给出M个查询字符串,问每个查询字符串在N个字符串中出现的次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
using namespace std;

int hashFunc(char S[], int len) { //将字符串S转换为整数
int id = 0;
for(int i = 0; i < len; i++)
id = id * 26 + (S[i] - 'A'); //将26进制转换为10进制
return id;
}

int main() {
int N, M;
int hashTable[26 * 26 * 26] = {0};
cin >> N >> M;
string S[N];
for(int i = 0; i < N; i++) {
cin >> S[i];
int id = hashFunc(S[i], 3);
hashTable[id]++; //字符串出现的次数+1
}
string tmp;
for(int i = 0; i < M; i++) {
cin >> tmp;
int id = hashFunc(tmp, 3);
cout << hashTable[id] << endl; //输出字符串出现的次数
}
return 0;
}

3.2.3 相关习题

题目 解题关键
B1005 继续(3n+1)猜想 奇数在判断过程中,可能大小会超过100
B1029 旧键盘
A1084 Broken Keyboard
待优化
B1033 旧键盘打字 不能用scanfcin读取字符串,应采用gets(str)getline(cin, str),因为题目只保证第 2 行输入的文字串非空
B1038 统计同成绩学生 \
B1039 到底买不买
A1092 To Buy or Not to Buy
\
B1042 字符统计 \
B1043 输出PATest \
B1047 编程团体赛 \
A1041
A1050
A1048

3.3 递归

3.3.1 分治

分治(divide and conquer)的全称为“分而治之”。分治法将原问题划分成若干个规模较小而结构与原问题相同或类似的子问题,然后分别解决这些子问题,最后合并子问题的解,即可得到原问题的解。分治法的三个步骤:

  1. 分解:将原问题分解为若干和原问题拥有相同或相似结构的子问题。
  2. 解决:递归求解所有子问题。如果存在子问题的规模小到可以直接解决,就直接解决它。
  3. 合并:将子问题的解合并为原问题的解。

分治法分解出的子问题应当是相互独立、没有交叉的。如果存在两个子问题有相交部分,就不应该使用分治法解决。分治法作为一种算法思想,既可以使用递归的手段实现,也可以通过非递归的手段实现。一般来说,使用 递归 实现较为容易


3.3.2 递归

递归很适合用来实现分治思想

递归的逻辑中一般有两个重要概念:

  1. 递归边界
  2. 递归调用

其中递归调用是将原问题分解为若干个子问题的手段,而递归边界则是分解的尽头。递归的代码结构中一定存在这两个该概念,它们支撑起了整个递归最关键的逻辑

经典例子1:使用递归求解n的阶乘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

int F(int n) {
if(n == 0) {
return 1;
} else {
return n * F(n-1);
}
}

int main() {
int n;
cin >> n;
cout << F(n) << endl;
return 0;
}

经典例子2:求 Fibonacci 数列的第n项

Fibonacci 数列(即斐波那契数列) 是满足 F(0) = 1, F(1) = 1, F(n) = F(n-1) + F(n-2) (n $\geq$ 2) 的数列,数列的前几项为 1, 1, 2, 3, 5, 8, 13, 21, … 。

从定义中可以获知递归边界为 F(0) = 1 和 F(1) = 1,且递归调用为 F(n) = F(n-1) + F(n-2) (n $\geq$ 2) ,因此可以仿照求解n的阶乘的写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

int F(int n) {
if(n == 0 || n == 1) { //递归边界
return 1;
} else {
return F(n-1) + F(n-2);
}
}

int main() {
int n;
cin >> n;
cout << F(n) << endl;
return 0;
}

最后来看全排列(Full Permutation)。全排列指n个整数能形成的 所有排列

现在需要按字典序从小到大的顺序输出$1\sim n$的全排列,其中$(a_1, a_2, …, a_n)$的字典序小于$(b_1, b_2, …, b_n)$是指存在一个$i$,使得$a_1=b_1$、$a_2 = b_2$、…、$a_{i-1} = b_{i-1}$、$a_i < b_i$成立。

从递归的角度考虑,就可以分为若干个子问题:“输出以1开头的全排列”、“输出以2开头的全排列” … “输出以n开头的全排列”。**不妨设定一个数组P,用来存放当前的排列;再设定一个散列数组hashTable**,其中hashTable[x],当整数x已经在数组P中时,为1

按顺序往P的第1位到第n位中填入数字。不妨假设当前已经填好了P[1] ~ P[index-1],正准备填P[index]。显然需要枚举 1 ~ n,如果当前枚举的数字x还没有在P[1] ~ P[index-1]中(即hashTable[x] == 0),那么就把它填入P[index],同时将hashTable[x]置为1,然后去处理P的第index + 1位(即进行递归);当递归完成时,再将hashTable[x]还原为0,以便让P[index]填下一个数字

那么递归边界是什么呢?显然,当index达到n + 1时,说明P的的第1 ~ n位都已经填好了,此时可以把数组P输出,表示生成了一个排列,然后直接return即可。下面给出n = 3时的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;

void generateP(int index, int n) {
if(index == n + 1) { //递归边界,已经处理完排列的 1~n 位
for(int i = 1; i <= n; i++)
cout << P[i];
cout << endl;
return;
}
for(int x = 1; x <= n; x++) { //枚举 1~n,试图将 x 填入 P[index]
if(hashTable[x] == 0) { //如果 x 不在 P[1] ~ P[index - 1] 中
P[index] = x; //令 P 的第 index 位为 x,即把x加如当前排列
hashTable[x] = 1; //记 x 已在 P 中
generateP(index + 1, n); //处理排列的第 index + 1 号位
hashTable[x] = 0; //已处理完 P[index] 为 x 的子问题,还原状态
}
}
}


int main() {
int n = 3, P[11], hashTable[11] = {0};
generateP(1); //从P[1]开始填
return 0;
}

最后的最后来看**$n$皇后问题。$n$皇后问题是指在一个$n*n$的国际象棋棋盘上放置$n$个皇后,使得这$n$个皇后两两均不在同一行、同一列、同一条对角线上**,求合法的方案数。

对于这个问题,如果采用组合数的方式来枚举每一种情况(即从$n^2$个位置中选择$n$个位置),那么需要$C_{n\times n}^n$的枚举量。但是换个思路,考虑到每行只能放置一个皇后、每列也只能放置一个皇后,如果把n列皇后所在的行号依次写出,那么就会是$1 \sim n$的一个排列。于是只需要枚举$1 \sim n$的所有排列,查看每个排列对应的放置方案是否合法,统计其中合法的方案即可。

于是可以在全排列的代码基础上进行求解。由于当达到递归边界时表示生成了一个排列,所以需要在起内部判断是否为合法方案,即遍历每两个皇后,判断它们是否在同一条对角线上(显然不在同一行和同一列),若不是,则累计计数变量cnt即可。主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int cnt = 0;

void generateP(int index) {
if(index == n + 1) { //递归边界,生成一个排列
int flag = 1; //flag为true表示当前排列为一个合法方案
for(int i = 1; i <= n; i++) { //遍历任意两个皇后
for(int j = i + 1; j <= n; j++) {
if(abs(i - j) == abs(P[i] - P[j])) //如果在一条对角线上
flag = 0;
}
}
if(flag) //当前方案合法
cnt++;
return;
}
for(int x = 1; x <= n; x++) {
if(hashTable[x] == 0) {
P[index] = x;
hashTable[x] = 1;
generateP(index + 1);
hashTable[x] = 0;
}
}
}

这种枚举所有情况,然后判断每一种情况是否合法的朴素做法称为暴力法

通过思考可以发现,当已经放置了一部分皇后时,可能剩余的皇后无论怎样放置都不可能合法,就不需要继续递归,直接返回上层即可。这种做法称为回溯法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void generateP(int index) {
if(index == n + 1) { //递归边界,生成一个 合法方案
cnt++;
return;
}
for(int x = 1; x <= n; x++) { //第 x 行
if(hashTable[x] == 0) { //第 x 行还没有皇后
int flag = 1; //flag 为 1 表示当前皇后不会和之前的皇后冲突
for(int pre = 1; pre < index; pre++) { //遍历之前的皇后
if(abs(index - pre) == abs(x - P[pre])) {
flag = 0; //与之前的皇后在一条对角线冲突
break;
}
}
if(flag) { //如果可以把皇后放在第 x 行
P[index] = x; //令第 index 列皇后的行号为 x
hashTable[x] = 1; //第 x 行已被占用
generateP(index + 1); //递归处理第 index + 1 行皇后
hashTable[x] = 0; //递归完毕,还原第 x 行 为 为占用
}
}
}
}

3.4 贪心

3.4.1 简单贪心

贪心法是求解一类最优化问题的方法,它总是考虑在当前状态下局部最优(或较优)的策略,来使全局的结果达到最优(或较优)。要获得最优结果,要求中间的每步策略都是最优的,因此严谨使用贪心法来求解最优化问题需要对采取的策略进行证明。证明的一般思路是使用反证法及数学归纳法,即假设策略不能导致最优解,然后通过一系列推导来得到矛盾,以此证明策略是最优的,最后用数学归纳法保证全局最优。不过对平常使用来说,也许没有时间或不太容易对想到的策略进行严谨的证明(贪心的证明往往比贪心本身更难),因此一般来说,如果在想到某个似乎可行的策略,并且自己无法举出反例,那么就勇敢的实现它


3.4.2 区间贪心

区间不相交问题:给出$N$个开区间$(x, y)$,从中选择尽可能多的开区间,使得这些开区间两两没有交集。例如对开区间$(1, 3)$、$(2,4)$、$(3, 5)$、$(6, 7)$来说,可以选出最多三个区间$(1, 3)$、$(3, 5)$、$(6, 7)$,它们互相没有交集。

首先考虑最简单的情况,如果开区间$I_1$被开区间$I_2$包含(如图4-5a所示),则选择$I_1$是最好的选择,因为这样就有更大的空间去容纳其他开区间

接下来把所有开区间按左端点$x$从大到小排序,如果去掉区间包含的情况,那么一定有$y_1 > y_2 > … > y_n$成立(如图4-5b所示)。观察发现,$I_1$右边有一段一定不会和其他区间重叠,如果把它去掉,那么$I_1$的左边剩余部分就会被$I_2$包含,由图4-5a的情况可知,应当选择$I_1$。因此对这种情况,总是先选择 左端点最大 的区间。值得注意的是,总是先选择右端点最小的区间的策略也是可行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <algorithm>
using namespace std;

const int maxn = 110;

struct Inteval {
int x, y; //开区间左右端点
} I[maxn];

bool cmp(Inteval a, Inteval b) {
if(a.x != b.x) { //先按左端点从大到小排序
return a.x > b.x;
else { //左端点相同的 按右端点从小到大排序
return a.y > b.y;
}
}

int main() {
int n;
while(cin >> n, n != 0) {
for(int i = 0; i < n; i++)
cin >> I[i].x >> I[i].y;
sort(I, I + n, cmp);
int cnt = 1, lastX = I[0].x; //记录不相交区间的个数,上一个被选中区间的左端点
for(int i = 1; i < n; i++) {
if(I[i].y <= lastX) { //如果该区间右端点在 lastX 左边
lastX = I[i].x; //以 I[i] 作为新选中的区间
cnt++; //不相交区间个数加1
}
cout << cnt << endl;
}
return 0
}

与这个问题类似的是区间选点问题:给出$N$个闭区间$[x, y]$,求最少需要确定多少个点,才能使每个闭区间中都至少存在一个点。例如对闭区间$[1,4]$、$[2, 6]$、$[5, 7]$来说,需要两个点(例如3、5)才能保证每个闭区间内都有至少一个点。

事实上,这个问题和区间不相交问题的策略是一致的。首先,回到图4-5a,如果闭区间$I_1$被闭区间$I_2$包含,那么在$I_1$中取点可以保证这个点一定在$I_2$内。接着把所有区间按左端点从大到小排序,去除掉区间包含的情况,就可以得到图4-5b。由于每个闭区间中都需要存在一个点,因此对左端点最大的区间$I_1$来说,取左端点就能让它尽可能多地覆盖其他区间。区间选点问题的代码只需要把区间不想交问题代码中的I[i].y <= lastX改为I[i].y < lastX即可。

总的来说,不是所有问题都适合使用贪心法,但是这并不妨碍贪心算法称为一个简洁、实用、高效的算法。


3.4.3 相关习题

题目 解题关键
B1020 月饼
A1070 Mooncake
库存量和售价都应该定义为double类型
B1023 组个最小数 \
A1033 To Fill or Not to Fill 能到达的距离内,由近到远遍历,有三种情况:

1. 最近距离的加油站都到不了
2. 出现油价比目前低的加油站,就直接去
2.1 油不够,只加能刚好到达的油量
2.2 不用加油
3. 没有更低价的加油站,就加满油(能尽量少加更贵的油),去价格相对 最低的加油站
A1037
A1038
A1067

3.5 二分

3.5.1 二分查找

二分查找的高效之处在于,每一步都可以去除当前区间中的一半元素,因此其时间复杂度是$O(\log n)$,这是十分优秀的。

在严格递增序列中查找给定的数$x$的做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

int binarySearch(int A[], int left, int right, int x) {
int mid;
while(left <= right) {
mid = (left + right) / 2;
if(A[mid] == x) {
return mid;
} else if(A[mid] > x) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return -1; //查找失败
}

int main() {
int A[n] = {1, 3, 4, 6, 7, 8, 10, 11, 12, 15};
printf("%d %d\n", binarySearch(A, 0, n - 1, 6), binarySearch(A, 0, n - 1, 9));
return 0;
}

需要注意的是,如果过二分上界超过int型数据范围的一半,当欲查询元素在序列较靠后位置时,语句mid = (left + right) / 2中的left + right有可能超过int而导致溢出,通常使用等价语句mid = left + (right - left) / 2作为代替,以避免溢出。另外,二分法可以使用递归进行实现,但在程序设计时,更多采用非递归的写法

接下来探讨更进一步的问题:如果递增序列$A$中的元素可能重复,那么如何对给定的欲查询元素$x$,**求出序列中第一个$\geq x$的元素位置$L$以及第一个$>x$的元素的位置$R$**,这样元素$x$在序列中的存在区间就是$[L, R)$。

例如对下标从0开始、有5个元素的序列${1,3,3,3,6}$来说,如果要查询3,则应当得到$L = 1$、$R = 4$;如果查询 5,则应当得到$L = R = 4$。如果序列中没有$x$,$L$和$R$也可以理解为 假设序列中存在$x$,则$x$应当在的位置

先来考虑第一个小问:求序列中的第一个$\geq x$的元素位置。

  1. A[mid] >= x,说明第一个$\geq x$的元素的位置一定在**mid处或mid的左侧,应往左子区间[left, mid]继续查询,即令right = mid**
  2. A[mid] < x,说明第一个$\geq x$的元素的位置一定在**mid右侧,应往右子区间[mid+1, right]继续查询,即令left = mid + 1**
1
2
3
4
5
6
7
8
9
10
11
12
int lower_bound(int A[], int left, int right, int x) {
int mid;
while(left < right) {
mid = (left + right) / 2;
if(A[mid] >= x) { //中间值 >= x
right = mid;
} else { //中间值 < x
left = mid + 1;
}
}
return left; //返回夹出来的位置
}

上述代码有两个需要注意的地方:

  1. 循环条件为left < right,而不是之前的left <= right,这是由问题本身决定的。因为如果想要返回第一个>= x的元素的位置,就不需要判断元素x本身是否存在
  2. 二分的初始区间应当能覆盖到所有可能返回的结果,考虑到欲查询元素可能比序列中所有元素都大,此时应当返回n,因此二分上界是n,故二分的初始区间为[left, right] = [0, n]

接下来解决第二小问:求序列中第一个大于x的元素的位置。

1
2
3
4
5
6
7
8
9
10
11
12
int upper_bound(int A[], int left, itn right, int x) {
int mid;
while(left < right) {
mid = (left + right) / 2;
if(A[mid] > x) {
right = mid;
} else {
left = mid + 1
}
}
return left; //返回夹出来的位置
}

lower_boundupper_bound函数都在解决这样一个问题:寻找有序序列中第一个满足某条件的元素的位置。这是一个非常重要且经典的问题,平时能碰到的大部分二分法问题都可以归结于这个问题。


3.5.2 二分法拓展

首先介绍如何计算$\sqrt2$的近似值

对$f(x) = x^2$来说,在$x \in [1, 2]$范围内,$f(x)$随着$x$的增大而增大,这就给使用二分法创造了条件

以精确到$10^{-5}$为例,令浮点型leftright初值分别是 1 和 2,然后根据中点mid处$f(x)$的值与 2 的大小来选择子区间进行逼近:

  1. f(mid) > 2,说明mid > $\sqrt2$,令right = mid
  2. f(mid) < 2,说明mid < $\sqrt2$,令left = mid

上面两个步骤当right - left < $10^{-5}$时结束,显然leftright的距离 < $10^{-5}$ 时已满足精度要求mid即为所求的近似值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const double eps = 1e-5; //精度为 10^{-5},科学计数法

double f(double x) {
return x * x;
}

double calSqrt() {
double left = 1, right = 2, mid;
while(right - left > eps) {
mid = (left + right) / 2;
if(f(mid) > 2) { //即 mid > sqrt(2)
right = mid;
} else {
left = mid;
}
}
return mid;
}

事实上,计算$\sqrt2$的近似值的问题是这样一个问题的特例:给定一个定义在$[L, R]$上的单调函数$f(x)$,求方程f(x) = 0的根

同样假设精度要求为eps = $10^{-5}$,函数$f(x)$在$[L,R]$上递增,leftright的初值分别为$L$、$R$。根据中点mid的函数值f(mid) 与 0 的大小关系来判断往哪个子区间继续逼近f(x) = 0的根:

  1. f(mid) > 0,说明f(x) = 0的根在mid左侧,应往左子区间继续逼近,即令right = mid
  2. f(mid) < 0,说明f(x) = 0的根在mid右侧,应往右子区间继续逼近,即令left = mid
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const double eps = 1e-5; //精度为 10^{-5},科学计数法

double f(double x) {
return ...;
}

double solve() {
double left = L, right = R, mid;
while(right - left > eps) {
mid = (left + right) / 2;
if(f(mid) > 0) {
right = mid;
} else {
left = mid;
}
}
return mid; //f(x) = 0 的根
}

3.5.3 快速幂

3.5.4 相关习题

题目 解题关键
A1010 Radix 1. 基数上界为 已确认数字的十进制大小 + 1 与 下界中的较大值(确保不会出现多个解(而且基数不确定的数,只有一位数的时候才可能多个解) )
2. 基数过大时,数值转换为十进制会发生上溢,存储结果为负数
A1044
A1048

3.6 tow pointers

3.6.1 什么是two pointers

3.6.2 归并排序

3.6.3 快速排序

3.6.4 相关习题

题目 解题关键
B1030 完美数列
A1085 Perfect Sequence
\
B1035 插入与归并
A1089 Insert or Merge
1. 插入排序:未排序部分和初始序列一定相同
2. 归并排序:末尾不足数量的子序列同样需要排序
3. 插入排序 更容易判断,将其作为判断排序类型的切入点
4. 插入和归并排序的**实际操作由排序函数sort/qsort**代替
A1029
A1048

3.7 其他高效技巧与算法

3.7.1 打表

3.7.2 活用递推

3.7.3 随机选择法

3.7.4 相关习题

题目 解题关键
B1040 有几个PAT
A1093 Count PAT’s
1. A前有P,后有T才能形成PAT;
2. 当前A能构成的PAT数量 = 之前P的数量 * 之后T的数量
3. 突破口:先遍历一遍,获取T的数量
B1045 快速排序
A1101 Quick Sort
1. 可能的主元:左侧的最大值比自身小;右侧的最小值比自身大
2. 输出即使没有主元,也得换行

4. 数学问题

4.1 简单数学

题目 解题关键
B1003
B1019
A1069
B1049
A1104
A1008
A1049

4.2 最大公约数与最小公倍数

题目 解题关键
B1008

4.3 分数的四则运算

题目 解题关键
B1034
A1088
A1081

4.4 素数

题目 解题关键
B1007
B1013
A1015
A1078

4.5 质因子分解

题目 解题关键
A1059
A1096

4.6 大整数运算

题目 解题关键
A1023
A1024

4.7 扩展欧几里得算法

4.8 组合数


5. C++标准模板库介绍

5.1 vector

题目 解题关键
A1039
A1047

5.2 set

题目 解题关键
A1063

5.3 string

题目 解题关键
A1060

5.4 map

题目 解题关键
B1044
A1100
A1022
A1054
A1071

5.5 queue

5.6 priority_queue

5.7 stack

5.8 pair

5.9 algorithm


6. 数据结构专题1

6.1 栈的应用

题目 解题关键
A1051 Pop Sequence 1. 可能会发生堆栈溢出
2. 栈顶元素和当前需要出栈的元素相同时,出栈。该判断之前必须先判断栈是否不为空(否则会报错)
3. 标记出栈序列中待出栈元素下标,最终标记只有超出序列下标,才可能是出栈序列

6.2 队列的应用

题目 解题关键
A1056 Mice and Rice 1. 输入的第二行是按老鼠的序号给出的重量。第三行给出的是老鼠的序号,而不是按序号给出的比赛序号
2. 用队列记录老鼠的序号(而不是记录老鼠的结构体,有利于减少内存的占用),晋级的老鼠重新入队
3. 最后的分组中,老鼠数量可能不足(利用<cmath>中的ceil函数向上取整)

6.3 链表处理

题目 解题关键
B1025 反转链表
A1074 Reversing Linked List
1. 通过数组下标来表示地址,便于链接各个节点
2. 可能存在无效结点
3. 只根据 链表顺序的地址 进行反转,有利于节约开销
A1032 Sharing \
A1052 Linked List Sorting 存在无有效结点的情况
A1097

7. 搜索专题

7.1 深度优先搜索DFS

题目 解题关键
A1103 Integer Factorization 1. 利用vector(可根据size函数确认符合要求的元素个数),预先存储所有 ≤ N的 元素(整数的P次幂值)
2. 为了保证底数最大的序列被选中,从大到小遍历
3. DFS函数的参数:当前元素下标,已选元素数量,已选元素之和,已选元素的底数之和
4. DFS函数的递归边界:已选元素数量为K且和为N
5. DFS函数的递归调用选择当前元素和不选择当前元素
6. 剪枝和超过N 或 已选元素数量超过K 或 遍历完底数序列时,不必再递归

7.2 广度优先搜索BFS

题目 解题关键
A1091 Acute Stroke 1. 设定相对当前位置的前后左右上下6个方向的增量,便于枚举
2. 设置数组inq记录当前位置是否入过队
3. 越界、当前位置为0、已入过队的位置无需再判断。
4. 在BFS函数中,利用队列进行BFS,使用STL的queuepush函数只将所选元素的副本入队,后续对原元素的操作不改变队列中的副本

8. 数据结构专题2

8.1 树与交叉树

8.2 二叉树的遍历

题目 解题关键
A1020 Tree Traversals \
A1086 Tree Traversals Again 先序序列相当于入栈次序;中序序列相当于出栈次序
A1102 Invert a Binary Tree 1. 题目给的是结点编号的关系,故采用静态链表存储二叉树
2. 反转二叉树即每个结点的左右子树对换,选择后序遍历

8.3 树的遍历

题目 解题关键
A1004 Counting Leaves 1. 利用数组存储每层的叶结点个数
2. 根据出队结点的层数,获取最大层数,遍历输出每层叶结点个数
A1053 Path of Equal Weight 1. 输出的路径序列要求降序,因此在读入结点时就将孩子节点按权重降序排序,这样,之后遍历时得到的路径即为降序

2. 利用vectorbeginend函数获得vector首元素地址尾后地址,用于sort函数排序
3. 利用vector存储路径,在递归子结点之前,用push_back函数将子结点加入路径,递归回溯后,用pop_back将先前加入的子结点移出路径
A1079 Total Sales of Supply Chain 1. 静态写法表示树,即用数组下标指代结点
2. 结点的结构体中设有货物量变量,用来判断是否为零售商(本题采用了BFS;也可DFS)
A1090 Highest Price in Supply Chain 由于不涉及结点的数据域,可以直接vector<int> child[maxn]存放树
A1094
A1106

8.4 二叉查找树BST

题目 解题关键
A1043 Is It a Binary Search Tree 1. 静态写法(变长数组)表示二叉树
2. 根据BST左子树所有结点的数据域 < 根结点的数据域,右子树所有结点的数据域 **≥** 根结点的数据域(**由输入样例得出大小关系**,镜像则相反),**获取左右子树的边界,判断是否符合先序序列,若符合**,则按照**左右根的顺序递归,将符合的根结点存入vector(获取后序序列)**
3. 若vecotr中元素数量 = 输入序列的元素数量(能顺利转换为BST的后序序列),说明输入序列为先序序列
A1064 Complete Binary Search Tree 1. 输入的元素升序排序,即为二叉树的中序遍历序列
2. 静态写法(变长数组)按照层序存储完全二叉排序树
3. 对二叉树进行中序遍历,依次填入中序遍历序列的元素即可得到层序存储的二叉树。注意:中序遍历时,二叉树数组根结点的下标
若设为1,则左右孩子结点下标为root * 2root * 2 + 1
若设为0,则左右孩子结点下标为root * 2 + 1root * 2 + 2
4. 顺序输出二叉树数组元素即为层序遍历序列
A1099 Build A Binary Search Tree 静态写法表示树,解题方法和 A1064 基本一致

8.5 平衡二叉树

题目 解题关键
A1066 Root of AVL Tree 即写出平衡二叉树的代码模板
1. 新建结点的初始高度为1
2. 获取树高时,注意递归到空结点时,返回0
3. 插入结点,递归过程中从系统栈出栈后
1) 平衡因子为2时,树型可能为LR型或LL型,LR型需要先对当前结点的左子树进行右旋;之后,LR型和LL型都需要对当前结点进行右旋
2) 平衡因子为-2时,树型可能为RL型或RR型,RL型需要先对当前结点的右子树进行左旋;之后,RL型和RR型都需要对当前结点进行左旋
4. 左旋和右旋过程中要及时更新树高

8.6 并查集

题目 解题关键
A1107 Social Clusters 1. 根据共同(包括潜在的→有共同爱好的人另外的爱好)爱好来确定潜在朋友的集合,利用hobby[1001]记录 任意一个 拥有对应爱好的结点便于合并操作
2. 遍历各用户,根据根结点统计各集合的人数
3. 将集合按包含人数降序排序,再遍历即可筛选统计出集合个数
4. 路径压缩可选

8.7 堆

题目 解题关键
A1098 Insertion or Heap Sort 1. 用于表示树的数组,注意下标从 0或1 开始,孩子结点表示的区别
2. 插入排序:未排序部分和初始序列一定相同
3. 堆排序:后面的元素有序,前面无规律
4. 插入排序 更容易判断,将其作为判断排序类型的切入点
4. 插入排序实际操作可由排序函数sort实现

5. 要用堆排序得到
递增序列
,则需要建立大根堆
6. 堆排序从后往前找到第一个 $<$ 堆顶的元素,与堆顶元素交换,再对整个堆向下调整

8.8 哈夫曼树


9. 图算法专题

9.1 图的定义和相关术语

9.2 图的存储

9.3 图的遍历

题目 解题关键
A1013 Battle Over Cities 1. 问题等价于求 一个无向图去掉一个结点后,连通分量的数量,这种问题一般有两种方法:图的遍历并查集
2. cin会超时,需要改为scanf,邻接矩阵和邻接表都可存储图
3. 每次输入被占领的城市之前,要重置城市的访问状态为未访问
4. 将
失去的城市标记为已访问
,即可达到失去的效果(没有公路可以到达)
A1021
A1034 Head of a Gang 1. 相同人员之间可能多次通话
2. map<type1, type2>自动按type1从小到大输出,使用map<string, int>建立头目姓名与成员数量的关系,便于输出结果
3. 图中可能有环,为了边权不被漏加,需要先累加边权再递归;同时为了防止边权被重复计算,累加后删除这条边,以免走回头路、重复计算边权
A1076 Forwards on Weibo 1. 转发是粉丝转发博主的信息,因此要根据用户的关注情况建立从博主到粉丝(即反向)有向图
2. DFS记录访问过的结点数量比较麻烦,BFS比较方便
3. 可能形成环 ,必须控制每个用户只能转发一次(遍历时只能访问一次)

9.4 最短路径

题目 解题关键
A1003 Emergency
A1018
A1030 Travel Plan
A1072
A1087

9.5 最小生成树

9.6 拓扑排序

9.7 关键路径


10. 动态规划专题

对黑盒测试来说,所有输入数据都放在一个文件里


单点测试

PAT试题基本为单点测试(即一次运行一组数据,一组数据通过测试则获得这组数据的分值)。


多点测试

多点测试要求程序一次运行所有数据,并要求输出结果都正确,只要存在错误,即不得分
大部分 在线判题系统(Online Judge) 都采用了多点测试
由于要求程序能运行所有数据,因此必须保证程序能反复执行代码的核心部分,这就要用到循环

输入类型

题目一般有3中输入的格式,需要采用不同输入方式。

  1. while··· EOF

题目没有给定输入的结束条件,则默认读取到文件末尾

EOF(End Of File)

可以通过printf("%d", EOF);读取EOF的数值,一般的设备上值是-1。
EOF操作:

  • windows:Ctrl + Z
  • unix:Ctrl + D

scanf 若成功读入一个值,返回1;若成功读入2个值,返回2;若读入失败,返回 -1,C/C++中,EOF即为 -1。

正常情况下,控制台中的输入不会失败,只有读取文件时到达文件末尾,导致无法读取时,才会产生读入失败

1
2
3
4
5
6
7
8
9
//读取数字、字符、字符串
while (scanf("%d", &n) != EOF) {
···
}

//读取字符串
while (gets(str) != NULL) {
···
}

  1. while··· break

题目要求,当输入的数据满足某个条件时,停止输入

1
2
3
4
5
6
7
8
9
10
11
while (scanf("%d%d", &a, &b) != EOF) {
if (a == 0 && b == 0) {
break;
}
printf("%d\n", a + b);
}

//更简洁的写法
while (scanf("%d%d", &a, &b), a || b) {// scanf()恒不为0
printf("%d\n", a + b);
}

  1. while (T--)

题目给出测试数据的组数,然后才给出相应数量数组的输入数据。

1
2
3
4
5
int T;
scanf("%d", &T);
while (T--) {
···
}

输出

  1. 正常输出

  2. 每组数据输出之后,额外加一个空行

  3. 两组输出数据之间有一个空行,最后一组数据后面没有空行

    一般在第三种输入类型while (T--)情况下出现,只需通过判断T是否为0选择是否输出额外的换行。


重置变量

在多点测试中,每一次循环都要重置变量和数组,否则在下一组数据来临时,变量和数组不是初始状态,将出错。重置数组一般使用memset函数或fill函数

0%