Python数据分析基础

注意:本文是根据b站视频总结的笔记,原视频在这里

matplotlib部分

matplotlib主要用于画图。

1. 先从一个简单的代码开始

我们收集了某城市某天内每隔2小时的气温值,准备画出一天气温变化图。

from matplotlib import pyplot as plt

x = range(2, 26, 2)  # x轴数据
y = [15, 13, 14.5, 17, 20, 25, 26, 26, 24, 22, 18, 15]  # y轴数据
plt.plot(x, y)  # 传入x和y,绘制折线图
plt.show()  # 展示图形

运行结果如下图:

默认运行效果

可以发现图已经画出来了,但是有时候这并不是我们想要的成品图,我们希望能对他进行一些个性化的操作。

2. 对图片进行一些调整

(1)设置图形大小

plt.figure(figsize=(20, 8), dpi=80)

如果觉得图片太小了,可以用上面的代码,设置长宽和dpi。

(2)调整坐标轴刻度

plt.xticks(ticks, labels)

常见用法如上。第一个参数ticks传递一个数组,通常直接传递x即可。第二个参数labels也是一个数组,且长度与x一般相同,用于对ticks中对应元素进行替换。例如在上面简单的代码中,加入如下内容:

from matplotlib import pyplot as plt

x = range(2, 26, 2)
y = [15, 13, 14.5, 17, 20, 25, 26, 26, 24, 22, 18, 15]

plt.figure(figsize=(12, 8), dpi=80)
plt.xticks(x)  # 只传入第一个参数

plt.plot(x, y)
plt.show()

显示结果如下:

xticks只传入一个参数

可以看到x轴的坐标已经按照x的值显示出来了。但是显示的只是数字,如果能显示“2点”,“4点”这样,会更加直观,这就需要用到第二个参数。将代码改成下面这样:

from matplotlib import pyplot as plt

x = range(2, 26, 2)
y = [15, 13, 14.5, 17, 20, 25, 26, 26, 24, 22, 18, 15]
x_ticks = ["{} o'clock".format(i) for i in x]

plt.figure(figsize=(12, 8), dpi=80)
plt.xticks(x, x_ticks)  # 传入两个参数,将x轴的值和字符对应起来

plt.plot(x, y)
plt.show()

显示结果如下:

xticks传入两个参数

这样一来,x轴的效果就很好了。但是y轴还是可以再改一下。由于我们没有设置y轴,因此系统默认为我们生成了一个。但是由于间隔为2,我们现在并不能很好的读取每一个点的值,希望能把y轴间隔弄小一点。同理,使用plt.yticks即可。

from matplotlib import pyplot as plt

x = range(2, 26, 2)
y = [15, 13, 14.5, 17, 20, 25, 26, 26, 24, 22, 18, 15]
x_ticks = ["{} o'clock".format(i) for i in x]

plt.figure(figsize=(12, 8))
plt.xticks(x, x_ticks)
plt.yticks(range(min(y), max(y) + 1))  # 根据y的值域显示y轴刻度

plt.plot(x, y)
plt.show()

显示效果如下:

更改y轴刻度

(3)增加网格

为了更加清楚的展示每个点的位置,我们可以为图形添加网格:

plt.grid()

添加网格后效果如下:

添加网格

(4)添加图形的描述并显示中文

接下来我们为图形增加坐标轴描述和标题:

plt.xlabel("时间")
plt.ylabel("温度:摄氏度")
plt.title("某地一天气温变化折线图")

如果直接增加这些代码,运行程序,会发现图虽然能显示出来,但是所有的汉字都变成了框框,而且python还报了一堆错。这主要是由于默认字体中不含中文字符造成的。我们可以手动修改字体,显示中文。

方法一:全局设置

import matplotlib

matplotlib.rc("font", family='Source Han Sans CN', weight="regular", size="12")

方法二:分内容设置

from matplotlib import font_manager

my_font = font_manager.FontProperties(fname="/usr/share/fonts/adobe-source-han-sans/SourceHanSansCN-Regular.otf")

plt.xlabel("时间", fontproperties=my_font)
plt.ylabel("温度:摄氏度", fontproperties=my_font)
plt.title("某地一天气温变化折线图", fontproperties=my_font)

我们采用全局设置,并将标题加粗,字号调大,修改完的代码如下:

from matplotlib import pyplot as plt
import matplotlib

matplotlib.rc("font", family='Source Han Sans CN', weight="regular", size="12")

x = range(2, 26, 2)
y = [15, 13, 14.5, 17, 20, 25, 26, 26, 24, 22, 18, 15]
x_ticks = ["{}点".format(i) for i in x]

plt.figure(figsize=(12, 8))
plt.grid()
plt.xticks(x, x_ticks)
plt.yticks(range(min(y), max(y) + 1))

plt.xlabel("时间")
plt.ylabel("温度:摄氏度")
plt.title("某地一天气温变化折线图", weight="bold", size="16")

plt.plot(x, y)
plt.show()

最后效果如图:

增加描述并设置中文

(5)保存图形

到这里基本的效果已经有了,除了直接输出图片,还可以保存图片:

plt.savefig(path)

这里可以保存为png格式,也可以保存为svg格式,按需求来就行。注意,plt.savefig()必须在画图之后在能保存,即应放在plt.plot()的后面。

3. 常见的图形的画法

(1)折线图

plt.plot(x, y, label, color, linestyle, ...)

案例:

画出两个人从11岁至30岁每年交女朋友的数量变化图
a = [1,0,1,1,2,4,3,2,3,4,4,5,6,5,4,3,3,1,1,1]
b = [1,0,3,1,2,2,3,3,2,1 ,2,1,1,1,1,1,1,1,1,1]

分析:

在同一幅图中画两条折线,只需要使用plot方法两次即可,同时为每一条线设置标签。

代码:

from matplotlib import pyplot as plt
import matplotlib

matplotlib.rc("font", family='Source Han Sans CN', weight="regular", size="12")

x = range(11, 31)
y1 = [1, 0, 1, 1, 2, 4, 3, 2, 3, 4, 4, 5, 6, 5, 4, 3, 3, 1, 1, 1]
y2 = [1, 0, 3, 1, 2, 2, 3, 3, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1]
x_ticks = ["{}岁".format(i) for i in x]

plt.figure(figsize=(12, 8))
plt.grid(alpha=0.4)  # 设置网格透明度
plt.xticks(x, x_ticks)

plt.xlabel("年龄")
plt.ylabel("女朋友数量")
plt.title("A和B从11岁至30岁女朋友数量变化图", weight="bold", size="16")

plt.plot(x, y1, label="A同学", color="#F08080")
plt.plot(x, y2, label="B同学", color="#DB7093", linestyle="--")
plt.legend()  # 显示图例
plt.show()

效果:

折线图

(2)散点图

plt.scatter(x, y, label, ...)

案例:

画出三月和十月每天气温的变化散点图

y3 = [11,17,16,11,12,11,12,6,6,7,8,9,12,15,14,17,18,21,16,17,20,14,15,15,15,19,21,22,22,22,23]

y10 = [26,26,28,19,21,17,16,19,18,20,20,19,22,23,17,20,21,20,22,15,11,15,5,13,17,10,11,13,12,13,6]

分析:

通过控制x将两个散点图画在同一张图里,便于比较

代码:

from matplotlib import pyplot as plt
import matplotlib

matplotlib.rc("font", family='Source Han Sans CN', weight="regular", size="12")

x3 = range(1, 32)
y3 = [
    11, 17, 16, 11, 12, 11, 12, 6, 6, 7, 8, 9, 12, 15, 14, 17, 18, 21, 16, 17,
    20, 14, 15, 15, 15, 19, 21, 22, 22, 22, 23
]
x10 = range(41, 72)
y10 = [
    26, 26, 28, 19, 21, 17, 16, 19, 18, 20, 20, 19, 22, 23, 17, 20, 21, 20, 22,
    15, 11, 15, 5, 13, 17, 10, 11, 13, 12, 13, 6
]

x = list(x3) + list(x10)
x_ticks = ["3月{}日".format(i) for i in x3] + ["10月{}日".format(i - 40) for i in x10]

plt.figure(figsize=(15, 10))
plt.xticks(x[::3], x_ticks[::3], rotation=45)

plt.xlabel("日期")
plt.ylabel("温度:摄氏度")
plt.title("三月和十月每天气温的变化散点图", weight="bold", size="16")

plt.scatter(x3, y3, label="三月", color="orange")
plt.scatter(x10, y10, label="十月", color="cyan")
plt.legend()
plt.show()

这里有两个点值得注意。第一个是通过控制x的范围将两个散点图在同一张图上分开显示,第二个就是对于x轴刻度的控制。因为如果31天全都写出来,会显得非常挤,所以在用plt.xticks做对应的时候,每隔3天对应一次,这样中间未对应的值就不会显示了,同时还将文本旋转45度便于查看。

效果:

散点图

(3)条形图

plt.bar(x, y, width)  # 纵向条形图
plt.barh(x, y, height)  # 横向条形图

I. 纵向条形图

案例:

画出2017年电影票房的条形图

a = ["战狼2","速度与激情8","功夫瑜伽","西游伏妖篇","变形金刚5:最后的骑士","摔跤吧!爸爸","加勒比海盗5:死无对证","金刚:骷髅岛","极限特工:终极回归","生化危机6:终章","乘风破浪","神偷奶爸3","智取威虎山","大闹天竺","金刚狼3:殊死一战","蜘蛛侠:英雄归来","悟空传","银河护卫队2","情圣","新木乃伊"]

b = [56.01,26.94,17.53,16.49,15.45,12.96,11.8,11.61,11.28,11.12,10.49,10.3,8.75,7.55,7.32,6.99,6.88,6.86,6.58,6.23]

代码:

from matplotlib import pyplot as plt
import matplotlib

matplotlib.rc("font", family='Source Han Sans CN', weight="regular", size="10")

x_ticks = [
    "战狼2", "速度与激情8", "功夫瑜伽", "西游伏妖篇", "变形金刚5:最后的骑士", "摔跤吧!爸爸", "加勒比海盗5:死无对证",
    "金刚:骷髅岛", "极限特工:终极回归", "生化危机6:终章", "乘风破浪", "神偷奶爸3", "智取威虎山", "大闹天竺",
    "金刚狼3:殊死一战", "蜘蛛侠:英雄归来", "悟空传", "银河护卫队2", "情圣", "新木乃伊"
]
x = range(len(x_ticks))
y = [
    56.01, 26.94, 17.53, 16.49, 15.45, 12.96, 11.8, 11.61, 11.28, 11.12, 10.49,
    10.3, 8.75, 7.55, 7.32, 6.99, 6.88, 6.86, 6.58, 6.23
]

plt.figure(figsize=(15, 10))
plt.xticks(x, x_ticks, rotation=45)

plt.xlabel("电影名称")
plt.ylabel("票房:亿元")
plt.title("2017年电影票房条形图", weight="bold", size="16")

plt.bar(x, y, 0.5)
plt.show()

效果:

纵向条形图

我们发现由于名称太长,即便旋转之后仍然难以阅读,故考虑使用横向条形图。

II. 横向条形图

代码:

from matplotlib import pyplot as plt
import matplotlib

matplotlib.rc("font", family='Source Han Sans CN', weight="regular", size="10")

y_ticks = [
    "战狼2", "速度与激情8", "功夫瑜伽", "西游伏妖篇", "变形金刚5:最后的骑士", "摔跤吧!爸爸", "加勒比海盗5:死无对证",
    "金刚:骷髅岛", "极限特工:终极回归", "生化危机6:终章", "乘风破浪", "神偷奶爸3", "智取威虎山", "大闹天竺",
    "金刚狼3:殊死一战", "蜘蛛侠:英雄归来", "悟空传", "银河护卫队2", "情圣", "新木乃伊"
]
y = range(len(y_ticks))
x = [
    56.01, 26.94, 17.53, 16.49, 15.45, 12.96, 11.8, 11.61, 11.28, 11.12, 10.49,
    10.3, 8.75, 7.55, 7.32, 6.99, 6.88, 6.86, 6.58, 6.23
]

plt.figure(figsize=(15, 10))
plt.yticks(y, y_ticks)

plt.xlabel("票房:亿元")
plt.ylabel("电影名称")
plt.title("2017年电影票房条形图", weight="bold", size="16")

plt.barh(y, x, 0.5)
plt.show()

效果:

横向条形图

我们发现在使用横向条形图的效果比纵向要好。不过要注意的是,在绘制横向条形图时,虽然图上x轴和y轴的相对位置没有变化,但是plt.barh()方法是先传y,再传x的。

III. 多个条形图

案例:

展示几部电影连续几天票房的对比情况

a = ["猩球崛起3:终极之战","敦刻尔克","蜘蛛侠:英雄归来","战狼2"]

b1 = [2358,399,2358,362]

b2 = [12357,156,2045,168]

b3 = [15746,312,4497,319]

分析:

我们同样可以通过改变x轴的位置让同一幅图中显示多个条形图

代码:

from matplotlib import pyplot as plt
import matplotlib

matplotlib.rc("font", family='Source Han Sans CN', weight="regular", size="10")

x_ticks = ["猩球崛起3:终极之战", "敦刻尔克", "蜘蛛侠:英雄归来", "战狼2"]
bar_width = 0.2

x1 = range(len(x_ticks))
x2 = [i + bar_width for i in x1]
x3 = [i + bar_width * 2 for i in x1]
x = list(x1) + x2 + x3
b1 = [2358, 399, 2358, 362]
b2 = [12357, 156, 2045, 168]
b3 = [15746, 312, 4497, 319]

plt.figure(figsize=(15, 10))
plt.xticks(x2, x_ticks)

plt.xlabel("电影名称")
plt.ylabel("票房:亿元")
plt.title("几部电影连续几天票房的对比情况", weight="bold", size="16")

plt.bar(x1, b1, bar_width, label="第一天")
plt.bar(x2, b2, bar_width, label="第二天")
plt.bar(x3, b3, bar_width, label="第三天")
plt.legend()
plt.show()

代码这里还有地方值得分析一下。为什么使用x1,x2,x3就可以做到这样的效果?在本代码中,x1 = [0, 1, 2, 3]x2 = [0.2, 1.2, 2.2, 3.2]x3 = [0.4, 1.4, 2.4, 3.4],由于每一条的宽度统一设置为0.2,意味着第一条的中点在0,在[-0.1, 0.1]的范围,第二条是以0.2为中点,在[0.1, 0.3]的范围内,第三条是以0.4为中点,在[0.3, 0.5]的范围内,这样我们就发现每一组的三条是紧挨在一起的。

第二个问题就是,三个plt.bar()代码分别画的是哪一部分呢?这个应该好理解,三条命令分别画的是蓝色、橙色和绿色,也就是说紧挨在一起的三条并不是同一条代码画出来的,而是先画每一组的第一条,再第二条,最后第三条。

效果:

多个条形图

(4)直方图

plt.hist(x, bins)  # 频数直方图
plt.hist(x, bins, density=True)  # 频率直方图

案例:

画出对250部电影时长分析的直方图

a = [131, 98, 125, 131, 124, 139, 131, 117, 128, 108, 135, 138, 131, 102, 107, 114, 119, 128, 121, 142, 127, 130, 124, 101, 110, 116, 117, 110, 128, 128, 115, 99, 136, 126, 134, 95, 138, 117, 111,78, 132, 124, 113, 150, 110, 117, 86, 95, 144, 105, 126, 130,126, 130, 126, 116, 123, 106, 112, 138, 123, 86, 101, 99, 136,123, 117, 119, 105, 137, 123, 128, 125, 104, 109, 134, 125, 127,105, 120, 107, 129, 116, 108, 132, 103, 136, 118, 102, 120, 114,105, 115, 132, 145, 119, 121, 112, 139, 125, 138, 109, 132, 134,156, 106, 117, 127, 144, 139, 139, 119, 140, 83, 110, 102,123,107, 143, 115, 136, 118, 139, 123, 112, 118, 125, 109, 119, 133,112, 114, 122, 109, 106, 123, 116, 131, 127, 115, 118, 112, 135,115, 146, 137, 116, 103, 144, 83, 123, 111, 110, 111, 100, 154,136, 100, 118, 119, 133, 134, 106, 129, 126, 110, 111, 109, 141,120, 117, 106, 149, 122, 122, 110, 118, 127, 121, 114, 125, 126,114, 140, 103, 130, 141, 117, 106, 114, 121, 114, 133, 137, 92,121, 112, 146, 97, 137, 105, 98, 117, 112, 81, 97, 139, 113,134, 106, 144, 110, 137, 137, 111, 104, 117, 100, 111, 101, 110,105, 129, 137, 112, 120, 113, 133, 112, 83, 94, 146, 133, 101,131, 116, 111, 84, 137, 115, 122, 106, 144, 109, 123, 116, 111,111, 133, 150]

分析:

直方图中涉及到一个组数的概念,组数 = 极差/组距,且最好是整除得到的组数。

代码:

from matplotlib import pyplot as plt
import matplotlib

matplotlib.rc("font", family='Source Han Sans CN', weight="regular", size="10")

a = [
    131, 98, 125, 131, 124, 139, 131, 117, 128, 108, 135, 138, 131, 102, 107,
    114, 119, 128, 121, 142, 127, 130, 124, 101, 110, 116, 117, 110, 128, 128,
    115, 99, 136, 126, 134, 95, 138, 117, 111, 78, 132, 124, 113, 150, 110,
    117, 86, 95, 144, 105, 126, 130, 126, 130, 126, 116, 123, 106, 112, 138,
    123, 86, 101, 99, 136, 123, 117, 119, 105, 137, 123, 128, 125, 104, 109,
    134, 125, 127, 105, 120, 107, 129, 116, 108, 132, 103, 136, 118, 102, 120,
    114, 105, 115, 132, 145, 119, 121, 112, 139, 125, 138, 109, 132, 134, 156,
    106, 117, 127, 144, 139, 139, 119, 140, 83, 110, 102, 123, 107, 143, 115,
    136, 118, 139, 123, 112, 118, 125, 109, 119, 133, 112, 114, 122, 109, 106,
    123, 116, 131, 127, 115, 118, 112, 135, 115, 146, 137, 116, 103, 144, 83,
    123, 111, 110, 111, 100, 154, 136, 100, 118, 119, 133, 134, 106, 129, 126,
    110, 111, 109, 141, 120, 117, 106, 149, 122, 122, 110, 118, 127, 121, 114,
    125, 126, 114, 140, 103, 130, 141, 117, 106, 114, 121, 114, 133, 137, 92,
    121, 112, 146, 97, 137, 105, 98, 117, 112, 81, 97, 139, 113, 134, 106, 144,
    110, 137, 137, 111, 104, 117, 100, 111, 101, 110, 105, 129, 137, 112, 120,
    113, 133, 112, 83, 94, 146, 133, 101, 131, 116, 111, 84, 137, 115, 122,
    106, 144, 109, 123, 116, 111, 111, 133, 150
]
d = 6  # 组距
bins = (max(a) - min(a)) // d  # 组数

plt.figure(figsize=(15, 10))
plt.grid()

plt.xlabel("时长")
plt.ylabel("数量")
plt.title("250部电影时长分析的直方图", weight="bold", size="16")
plt.xticks(range(min(a), max(a) + d, d))

plt.hist(a, bins)  # 频数直方图
plt.show()

效果:

频数直方图

(5)其他

更多图表见官网:https://matplotlib.org/gallery/index.html

numpy部分

numpy主要用于处理数值型数据。

(1)使用numpy生成数组

import numpy as np

a = np.array([1, 2, 3, 4, 5])
b = np.array(range[1, 6])
c = np.arange((1, 6))

上面的abc的值是一样的,都是一个类型为numpy.ndarray的数组[1 2 3 4 5]

(2)数组的形状

import numpy as np

t1 = np.arange(12)
t2 = np.array([[1, 2, 3], [4, 5, 6]])
>>> t1
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])

>>> t2
array([[1, 2, 3],
       [4, 5, 6]])

查看数组的形状:

>>> t1.shape
(12,)

>>> t2.shape
(2, 3)

shape属性返回的是一个元组,说明了当前数组的形状。可以看出:

  • 元组的元素个数表示了数组的维数
  • 元组的第一个元素表示第一层数组所含元素个数,第二个元素表示第二层数组所含元素个数,以此类推
  • 当数组为二维时,第一个元素即为行数,第二个元素即为列数

修改数组的形状

>>> t1.reshape((3, 4))  # 将t1变成一个3行4列的数组
array([[0, 1, 2, 3],
       [4, 5, 6, 7],
       [8, 9, 10, 11]])

>>> t1.reshape((3, 5))  # 试图将t1变成一个3行5列的数组
Traceback (most recent call last):
  File "", line 1, in 
ValueError: cannot reshape array of size 12 into shape (3,5)

可以看出,reshape方法传递一个表示维数的元组,就会返回一个按要求修改的新数组,而不改变原数组。如果无法修改,则会报错。

除了将低维数组变成高维,还可以降维:

>>> t2.reshape((6,))
array([1, 2, 3, 4, 5, 6])

>>> t2.flatten()
array([1, 2, 3, 4, 5, 6])

降维除了使用reshape,如果直接降成一维,可以使用flatten方法。

(3)数组的计算

数组与数字计算:

与数字进行计算时,会把计算应用到数组中的每一个元素,这被称为“广播机制”。

数组与同维度数组计算:

与同维度数组进行计算时,会让每一个对应的元素进行计算。

数组与不同维度的数组进行计算:

>>> t3 = np.arange(12).reshape((3, 4))
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])

>>> t4 = np.arange(3).reshape((3, 1))
array([[0],
       [1],
       [2]])
S
>>> t5 = np.arange(4)
array([0, 1, 2, 3])

>>> t3 + t4
array([[ 0,  1,  2,  3],
       [ 5,  6,  7,  8],
       [10, 11, 12, 13]])

>>> t3 + t5
array([[ 0,  2,  4,  6],
       [ 4,  6,  8, 10],
       [ 8, 10, 12, 14]])

可以看到,尽管两个数组的维数不同,但如果在横向上维度相同,则在横向上进行运算,如果在竖向上维度相同,则在竖向上进行运算。

事实上广播原则并不局限于与数字计算时。如果两个不同维度的数组没有1维的方面,两个数组仍然是可以计算的。例如:a = (3,3,2)可以和b = (3,2)进行运算,而不能和c = (3,3)进行运算。因为a中有三个(3,2)的数组,可以使用广播机制,而a中没有(3,3)的数组,无法运算。

(4)numpy读取文件

数据分析中,一般使用的是csv文件,即逗号分隔值文件。

np.loadtxt(frame, dtype=np.float, delimiter=None, skiprows=0, usecols=None, unpack=False)
参数 解释
frame 文件、字符串或产生器,可以是gz或bz2压缩文件
dtype 数据类型,可选,默认为np.float
delimiter 分隔字符串,默认是任何空格,若读取csv文件则改为逗号
skiprows 跳过前x行
usecols 读取指定的列,传入列索引的元组
unpack 如果为True,则对源文件数据转置,如果为False,则保持原文件数据不变

(5)numpy中的转置

t.transpose()
t.T
t.swapaxis(1, 0)

以上三种方法都可以实现二位数组t的转置。

(6)numpy的索引和切片

# 取行
t[2]

# 取连续的多行
t[2:5]

# 取不连续的多行
t[[2, 8, 10]]

# 取列
t[:, 2]

# 取连续的多列
t[:, 2:5]

# 取不连续的多列
t[:, [2, 8, 10]]

# 同时取行和列
t[3, 4]

# 取多行和多列(3-5行,2-4列)
t[2:5, 1:4]

# 取多个不相邻的点
t[[0, 2, 5], [2, 4, 8]]

(7)numpy中的布尔索引

>>> t = np.arange(24).reshape((4, 6))
>>> t < 10
array([[ True,  True,  True,  True,  True,  True],
       [ True,  True,  True,  True, False, False],
       [False, False, False, False, False, False],
       [False, False, False, False, False, False]])

>>> t[t < 10] = 3
array([[ 3,  3,  3,  3,  3,  3],
       [ 3,  3,  3,  3, 10, 11],
       [12, 13, 14, 15, 16, 17],
       [18, 19, 20, 21, 22, 23]])

>>> t[t > 20]
array([21, 22, 23])

还可以通过whereclip三元运算符进行修改。

>>> t
array([[ 3,  3,  3,  3,  3,  3],
       [ 3,  3,  3,  3, 10, 11],
       [12, 13, 14, 15, 16, 17],
       [18, 19, 20, 21, 22, 23]])

>>> np.where(t<=3, 0, 100)  # t小于等于3的元素替换为0,否则替换为100
array([[  0,   0,   0,   0,   0,   0],
       [  0,   0,   0,   0, 100, 100],
       [100, 100, 100, 100, 100, 100],
       [100, 100, 100, 100, 100, 100]])

>>> t.clip(10, 18)  # t中小于10的替换为10,大于18的替换为18
array([[10, 10, 10, 10, 10, 10],
       [10, 10, 10, 10, 10, 11],
       [12, 13, 14, 15, 16, 17],
       [18, 18, 18, 18, 18, 18]])

值得注意的是whereclip都会返回一个新数组,而不修改原来的数组。

(8)数组的拼接

>>> t1 = np.arange(12).reshape((2, 6))
array([[ 0,  1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10, 11]])

>>> t2 = np.arange(12, 24).reshape((2, 6))
array([[12, 13, 14, 15, 16, 17],
       [18, 19, 20, 21, 22, 23]])

>>> np.vstack((t1, t2))  # 竖直拼接
array([[ 0,  1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10, 11],
       [12, 13, 14, 15, 16, 17],
       [18, 19, 20, 21, 22, 23]])

>>> np.hstack((t1, t2))  # 水平拼接
array([[ 0,  1,  2,  3,  4,  5, 12, 13, 14, 15, 16, 17],
       [ 6,  7,  8,  9, 10, 11, 18, 19, 20, 21, 22, 23]])

(9)数组的行列交换

>>> t = np.arange(12, 24).reshape((3, 4))
array([[12, 13, 14, 15],
       [16, 17, 18, 19],
       [20, 21, 22, 23]])

>>> t[[1, 2],:] = t[[2, 1],:]  # 行交换
array([[12, 13, 14, 15],
       [20, 21, 22, 23],
       [16, 17, 18, 19]])

>>> t[:, [0, 2]] = t[:, [2, 0]]  # 列交换
array([[14, 13, 12, 15],
       [22, 21, 20, 23],
       [18, 17, 16, 19]])

(10)numpy中的轴

轴(axis)使用0,1,2,……等数字进行表示。在一个形状为(2,)的一维数组中,只有0轴,长度为2;在形状为(2,3)的二维数组中,有0轴和1轴,0轴长度为2,1轴长度为3,等等。

(11)numpy中的nan和inf

nan(Not a Number):不是一个数字。

在以下情况可能出现nan:

  • 读取本地文件为float格式,如果有缺失,则显示nan
  • 做了不合适的运算,例如0 / 0 或inf - inf

关于nan,需要注意的有:

  • type(np.nan)为float类型
  • np.nan == np.nan返回False,np.nan != np.nan返回True
  • nan和任何值计算都是nan
  • 可以使用np.isnan(t)判断一个数是不是nan或一个数组中是否含有nan
  • 可以使用np.count_nonzero(t!=t)np.count_nonzero(np.isnan(t))计算数组中nan的个数

inf(infinity):无穷大,分为+inf和-inf。

在以下情况可能出现inf:

  • 一个数字除以0

nan和inf都是float类型

(12)numpy中的常用统计函数

方法 说明
t.sum(axis=None) 求和
t.mean(axis=None) 平均值
np.median(t, axis=None) 中位数
t.max(axis=None) 最大值
t.mim(axis=None) 最小值
np.ptp(t, axis=None) 极差
t.std(axis=None) 标准差

(13)其他方法

方法 说明
t.astype(int) 修改数组t的数据类型
np.argmax(t, axis=0) 获取数组t在0轴上的最大值的位置
np.argmin(t, axis=1) 获取数组t在1轴上的最小值的位置
np.zeros((3, 4)) 创建一个全0的数组
np.ones((3, 4)) 创建一个全1的数组
np.eye(3) 创建一个三阶单位矩阵

pandas部分

pandas主要用于处理字符串数据。

pandas的常用数据类型:

  • Series:一维,带标签数组
  • DataFrame:二维,Series容器

(1)创建Series

import pandas as pd
import numpy as np

# 通过列表创建Series
t1 = pd.Series([1, 2, 3, 4, 5], index=list("abcde"))

# 通过字典创建Series
t2 = pd.Series({"name": "zhangsan", "age": 18, "tel": 10086})

# 通过numpy数组创建Series
t3 = pd.Series(np.arange(5))

如果不指定index,则默认是0,1,2,…。如果是字典类型创建的,则index为字典的key。

>>> t2
name    zhangsan
age           18
tel        10086
dtype: object

>>> t3
0    0
1    1
2    2
3    3
4    4
dtype: int64

>>> type(t2)


>>> 0    0.0
1    1.0
2    2.0
3    3.0
4    4.0
dtype: float64

Series有两个属性,分别是indexvalues,可以获取到一个Series的索引和值。

>>> t2.index
Index(['name', 'age', 'tel'], dtype='object')

>>> t2.values
array(['zhangsan', 18, 10086], dtype=object)

(2)Series的切片和索引

>>> t2["age"]
18

>>> t2[0]
'zhangsan'

>>> t2[1:]
age       18
tel    10086
dtype: object

>>> t2[[0, 2]]
name    zhangsan
tel        10086
dtype: object

>>> t3[t3>=3]
3    3
4    4
dtype: int64

(3)创建DataFrame

>>> t1 = pd.DataFrame(np.arange(12).reshape((3, 4)))
>>> t1
   0  1   2   3
0  0  1   2   3
1  4  5   6   7
2  8  9  10  11

>>> t1 = pd.DataFrame(np.arange(12).reshape((3, 4)), index=list("abc"), columns=list("WXYZ"))
>>> t1
   W  X   Y   Z
a  0  1   2   3
b  4  5   6   7
c  8  9  10  11

>>> t2 = pd.DataFrame({"name": ["zhangsan", "lisi"], "age": [18, 22], "tel": [10086, 10000]})
>>> t2
       name  age    tel
0  zhangsan   18  10086
1      lisi   22  10000

>>> t3 = pd.DataFrame([{"name": "zhangsan", "age": 18, "tel": 10086}, {"name": "lisi", "age": 18}])
>>> t3
       name  age      tel
0  zhangsan   18  10086.0
1      lisi   18      NaN

可以看到3*4的矩阵外面多出来一列和一行,竖着的一列叫index,即行索引,axis=0;横着的一行叫columns,即列索引,axis=1。

创建DataFrame时,可以传入一个每个元素是列表的字典,也可以传入一个每个元素是字典的列表,两者相同。且没有对应索引的值处显示NaN。

(4)DataFrame的描述信息

>>> t3.index
RangeIndex(start=0, stop=2, step=1)

>>> t3.columns
Index(['name', 'age', 'tel'], dtype='object')

>>> t3.values
array([['zhangsan', 18, 10086.0],
       ['lisi', 18, nan]], dtype=object)

>>> t3.shape
(2, 3)

>>> t3.dtypes
name     object
age       int64
tel     float64
dtype: object
    
>>> t3.ndim  # 显示数据维度
2

除此之外,还有:

方法 说明
df.head(n) 显示头部几行,默认5行
df.tail(n) 显示尾部几行,默认5行
df.info() 显示行数、列数、列索引、列非空值个数、列类型、内存占用
df.describe() 计数、均值、标准差、最大值、四分位数、最小值
df.sort_values(by="", ascending=False) 按照by进行排序,ascending默认为True,即升序

(5)DataFrame的索引

df[:]  # 取行,结果为DataFrame类型
df[""]  # 取列,结果为Series类型

df[:100]  # 取前100行
df["count_AnimalName"]  # 取count_AnimalName列
df[20:100]["count_AnimalName"]  # 取20-100行的count_AnimalName列

当然还有其他的方法:loc和iloc:

loc:通过标签获取数据

>>> t
   W  X   Y   Z
a  0  1   2   3
b  4  5   6   7
c  8  9  10  11

>>> t.loc["a", "W"]
0

>>> t.loc["a", ["W", "Z"]]
W    0
Z    3
Name: a, dtype: int64

>>> t.loc["a":"c", ["W", "Z"]]
   W   Z
a  0   3
b  4   7
c  8  11

iloc:通过位置获取数据

>>> t.iloc[:1, [0, 2]]
   W  Y
a  0  2

>>> t.iloc[:, 2]
a     2
b     6
c    10
Name: Y, dtype: int64

(6)pandas的布尔索引

>>> t[t["X"]>4]
   W  X   Y   Z
b  4  5   6   7
c  8  9  10  11

>>> t[(t["W"]>0) & (t["Z"]<8)]
   W  X  Y  Z
b  4  5  6  7

(7)pandas的字符串方法

pandas有许多字符串操作方法,例如我想取出Row_Labels列中字符串长度大于4的行,可以:

df[df["Row_Labels"].str.len() > 4]

通过df.str.方法()的方式进行调用。具体方法列表此处不展开。

(8)pandas缺失数据的处理

先来看这样一组数据:

>>> t
      U     V     W     X     Y     Z
A   NaN   1.0   2.0   3.0   4.0   NaN
B   6.0   7.0   8.0   9.0   0.0  11.0
C  12.0  13.0  14.0  15.0  16.0  17.0
D  18.0  19.0   NaN  21.0  22.0  23.0

数据缺失一般有两种情况,一种是NaN(即np.nan),一种是0。

判断数据是否为nan:

>>> pd.isnull(t)
       U      V      W      X      Y      Z
A   True  False  False  False  False   True
B  False  False  False  False  False  False
C  False  False  False  False  False  False
D  False  False   True  False  False  False

>>> pd.notnull(t)
       U     V      W     X     Y      Z
A  False  True   True  True  True  False
B   True  True   True  True  True   True
C   True  True   True  True  True   True
D   True  True  False  True  True   True

nan处理方法:

方法一:选出不含nan的行列

>>> t1 = t[pd.notnull(t["U"])]
>>> t1
      U     V     W     X     Y     Z
B   6.0   7.0   8.0   9.0   0.0  11.0
C  12.0  13.0  14.0  15.0  16.0  17.0
D  18.0  19.0   NaN  21.0  22.0  23.0

方法二:删除含有nan的行列

df.dropna(axis=0, how="any", inplace=False)

axis指定操作的轴;how默认为any,即一整行只要包含nan就删除,可以改成all,表示只有一整行全都是nan才删除;inplace表示是否将结果直接赋给原变量,默认为False。

>>> t.dropna(how="any")
      U     V     W     X     Y     Z
B   6.0   7.0   8.0   9.0   0.0  11.0
C  12.0  13.0  14.0  15.0  16.0  17.0

方法三:用值填充nan

df.fillna(value)

一般可以使用均值、中位数和0填充nan。

>>> t.fillna(t.mean())
      U     V     W     X     Y     Z
A  12.0   1.0   2.0   3.0   4.0  17.0
B   6.0   7.0   8.0   9.0   0.0  11.0
C  12.0  13.0  14.0  15.0  16.0  17.0
D  18.0  19.0   8.0  21.0  22.0  23.0

我们会发现,对含有nan的列计算平均值时,会自动忽略nan,计算余下值的平均值。这与numpy中不同,在numpy中,nan与任何值运算都是nan。

0值处理方法:

对于不需要处理的0(比如实际值就是0)我们可以不处理;对于需要处理的0,可以统一赋值为nan:

t[t==0] = np.nan

需要注意的是,0会参与均值和中位数的运算。

(9)数据合并

join:把行索引相同的数据合并到一起

>>> t1 = pd.DataFrame(np.zeros((2, 5)), index=list("AB"), columns=list("VWXYZ"))
>>> t1
     V    W    X    Y    Z
A  0.0  0.0  0.0  0.0  0.0
B  0.0  0.0  0.0  0.0  0.0

>>> t2 = pd.DataFrame(np.ones((3, 4)), index=list("ABC"))
>>> t2
     0    1    2    3
A  1.0  1.0  1.0  1.0
B  1.0  1.0  1.0  1.0
C  1.0  1.0  1.0  1.0

>>> t2.join(t1)
     0    1    2    3    V    W    X    Y    Z
A  1.0  1.0  1.0  1.0  0.0  0.0  0.0  0.0  0.0
B  1.0  1.0  1.0  1.0  0.0  0.0  0.0  0.0  0.0
C  1.0  1.0  1.0  1.0  NaN  NaN  NaN  NaN  NaN

>>> t1.join(t2)
     V    W    X    Y    Z    0    1    2    3
A  0.0  0.0  0.0  0.0  0.0  1.0  1.0  1.0  1.0
B  0.0  0.0  0.0  0.0  0.0  1.0  1.0  1.0  1.0

可以发现,当t2.join(t1)时,t1的AB行内容直接增加在后面,新增了VWXYZ列,而空缺的C行全为NaN。当t1.join(t2)时,t2的C行直接被删去了。故谁调用join方法,就以谁为基础。

merge:按照指定的列把数据按照一定方式合并到一起

首先我们定义两个新的数据:

>>> df1 = pd.DataFrame(np.ones((2, 4)), index=list("AB"), columns=list("abcd"))
>>> df2 = pd.DataFrame(np.arange((3, 3)), columns=list("fax"))

>>> df1
     a    b    c    d
A  1.0  1.0  1.0  1.0
B  1.0  1.0  1.0  1.0

>>> df2
   f  a  x
0  0  1  2
1  3  4  5
2  6  7  8

然后执行下面的命令:

>>> df1.merge(df2, on="a", how="inner")
     a    b    c    d  f  x
0  1.0  1.0  1.0  1.0  0  2
1  1.0  1.0  1.0  1.0  0  2

我们把df2合并到df1中,并指定参数on为a,意思是按照a列进行合并,参数how的值为inner,表示取交集,inner亦为该参数的默认值。

df1中a列全为1,df2中只有0行为1,1行和2行与df1并无交集,所以df2中0行的f和x列均被合并至df1中的0行和1行。

如果将df2的[1, "a"]处修改为1,再执行该代码,结果如下:

>>> df2.loc[1, "a"] = 1
>>> df2
   f  a  x
0  0  1  2
1  3  1  5
2  6  7  8

>>> df1.merge(df2, on="a")
     a    b    c    d  f  x
0  1.0  1.0  1.0  1.0  0  2
1  1.0  1.0  1.0  1.0  3  5
2  1.0  1.0  1.0  1.0  0  2
3  1.0  1.0  1.0  1.0  3  5

可以看到,df2的a列中有两个1,分别是0行和1行,合并后的结果共有四行,分别是把df2的0行和1行合并至df1的0行,再把df2的0行和1行合并至df1的1行。

下面是how为outer的情况,outer表示取并集:

>>> df1.loc["A", "a"] = 100
>>> df1
       a    b    c    d
A  100.0  1.0  1.0  1.0
B    1.0  1.0  1.0  1.0
>>> df2
   f  a  x
0  0  1  2
1  3  1  5
2  6  7  8

>>> df1.merge(df2, on="a", how="outer")
       a    b    c    d    f    x
0  100.0  1.0  1.0  1.0  NaN  NaN
1    1.0  1.0  1.0  1.0  0.0  2.0
2    1.0  1.0  1.0  1.0  3.0  5.0
3    7.0  NaN  NaN  NaN  6.0  8.0

我们会发现,在合并后的第0行,df1的a是100,而df2中没有a为100的行,故以df1为准,空缺位置补NaN。1行和2行与上面的例子相同,不再解释。由于df1仅有2行,又此处是outer,故3行以df2为主,df1空缺处补NaN。

还有左连接和右连接,分别对应left和right,结果如下:

>>> df1.merge(df2, on="a", how="left")
       a    b    c    d    f    x
0  100.0  1.0  1.0  1.0  NaN  NaN
1    1.0  1.0  1.0  1.0  0.0  2.0
2    1.0  1.0  1.0  1.0  3.0  5.0

>>> df1.merge(df2, on="a", how="right")
     a    b    c    d  f  x
0  1.0  1.0  1.0  1.0  0  2
1  1.0  1.0  1.0  1.0  3  5
2  7.0  NaN  NaN  NaN  6  8

这两种模式可以理解为outer的细分,left就是在outer的基础上完全以df1为准,故在outer的结果上删除了3行。right就是在outer的基础上完全以df2为准,所以[0, "a"]位置上的值是1而不是100。

除了直接指定on,还可以分别指定left_on和right_on,例如:

>>> df1.merge(df2, left_on="a", right_on="f")
Empty DataFrame
Columns: [a_x, b, c, d, f, a_y, x]
Index: []

可以看到返回了一个空结果,因为df1的a列和df2的f列并无交集。

(10)数据分组聚合

有时候我们有一组庞大的数据,需要对数据进行分组研究(比如按国家,按性别等等),可以使用分组聚合。

df = pd.read_csv(file_path)
grouped = df.groupby(by="Country")  # 按country字段进行分组

grouped是一个DataFrameGroupBy类型的对象,可以进行遍历或调用聚合方法。

遍历:

for i in grouped:
    print(i)

for i, j in grouped:
    print(i)
    print(j)

每一个i是一个元组,第一个元素是Country的值,第二个元素是一个DataFrame,保存了该Country下的所有其他列的信息。也可以用下面的方法分别遍历第一个元素和第二个元素。

分组之后,可以调用许多聚合方法:

print(grouped.count())  # 统计每个国家的其他所有列总数
print(grouped["Brands"].count())  # 统计每个国家的Brands总数

除此之外,还有平均值、中位数等聚合方法。

方法 说明
count 非NaN值的数量
sum 非NaN值的和
mean 非NaN值的平均数
median 非NaN值的算数中位数
std、var 无偏(分母为n-1)标准差和方差
min、max 非NaN值的最小值和最大值

还可以按照多个条件进行分组:

# 分别按照国家和省份进行分组,返回Series
grouped = df.groupby(by=["Country", "State/Province"])   

# 分别按照国家和省份进行分组,并只取Brand列数据,返回Series
grouped = df["Brand"].groupby(by=[df["Country"], df["State/Province"]])
grouped = df.groupby(by=["Country", "State/Province"])["Brand"]

# 分别按照国家和省份进行分组,并只取Brand列数据,返回DataFrame
grouped = df[["Brand"]].groupby(by=[df["Country"], df["State/Province"]])   
grouped = df.groupby(by=["Country", "State/Province"])[["Brand"]]

上面的例子中,如果返回的是Series,输出之后会发现仍然有三行,但却是Series类型。前两列都是索引,被称为复合索引。

(11)复合索引

简单的索引操作:

  • 获取索引:df.index
  • 指定索引:df.index = ["x", "y"]
  • 重新设置索引:df.reindex(list("abcd")),类似于从df中取出索引为a、b、c、d的四行,如果不存在则全为NaN
  • 指定某一列作为索引:df.set_index("Country", drop=False),drop为False表示在df中仍然保留Country列的内容
  • 返回索引的唯一值:df.index.unique(),索引可以重复,unique()方法同样适用于索引

但假如我使用df.set_index(["a", "b"])会怎样呢?

>>> df = pd.DataFrame(np.arange(12).reshape((3, 4)), columns=list("abcd"))
>>> df
   a  b   c   d
0  0  1   2   3
1  4  5   6   7
2  8  9  10  11

>>> df1 = df.set_index(["a", "b"])
>>> df1
      c   d
a b        
0 1   2   3
4 5   6   7
8 9  10  11

>>> df1.index
MultiIndex([(0, 1),
            (4, 5),
            (8, 9)],
           names=['a', 'b'])

我们会发现df1中有两个索引,分别是a和b,这就是复合索引。

对于Series的复合索引,可以使用t["a", "b"]的方式来取值,对于DataFrame的复合索引,可以使用df.loc["a"].loc["b"]的方式来取值。如果我们希望先取第二层索引,再取第一层索引,可以通过df.swaplevel()交换索引次序。

(12)pandas的时间序列

pd.date_range(start=None, end=None, periods=None, freq="D")

其中:

  • start:开始时间
  • end:结束时间
  • periods:重复次数
  • freq:频率

我们来看几个例子:

>>> pd.date_range(start="20200601", end="20200701", freq="D")
DatetimeIndex(['2020-06-01', '2020-06-02', '2020-06-03', '2020-06-04',
               '2020-06-05', '2020-06-06', '2020-06-07', '2020-06-08',
               '2020-06-09', '2020-06-10', '2020-06-11', '2020-06-12',
               '2020-06-13', '2020-06-14', '2020-06-15', '2020-06-16',
               '2020-06-17', '2020-06-18', '2020-06-19', '2020-06-20',
               '2020-06-21', '2020-06-22', '2020-06-23', '2020-06-24',
               '2020-06-25', '2020-06-26', '2020-06-27', '2020-06-28',
               '2020-06-29', '2020-06-30', '2020-07-01'],
              dtype='datetime64[ns]', freq='D')

>>> pd.date_range(start="20200601", end="20200701", freq="10D")
DatetimeIndex(['2020-06-01', '2020-06-11', '2020-06-21', '2020-07-01'], dtype='datetime64[ns]', freq='10D')

>>> pd.date_range(start="20200601", periods=10, freq="M")
DatetimeIndex(['2020-06-30', '2020-07-31', '2020-08-31', '2020-09-30',
               '2020-10-31', '2020-11-30', '2020-12-31', '2021-01-31',
               '2021-02-28', '2021-03-31'],
              dtype='datetime64[ns]', freq='M')

关于频率的更多缩写:

别名 说明
D 每日历日
B 每工作日
H 每小时
T或min 每分
S 每秒
L或ms 每毫秒
U 每微秒
M 每月最后一个日历日
BM 每月最后一个工作日
MS 每月第一个日历日
BMS 每月第一个工作日

通过下面的命令,可以把时间字符串转换为时间序列:

df["timeStamp"] = pd.to_datetime(df["timeStamp"], format="")

其中df["timpStamp"]中原本为时间字符串,使用pd.to_datetime()可以转化为时间索引。format一般不用写,除非时间格式特殊(比如包含中文)。

(13)重采样

可以使用resample方法将时间序列从一个频率转化为另一个频率。注意该方法仅针对时间为索引的情况,如果时间不为索引,可以用df.set_index("timeStamp")进行设置。

将高频率数据转化为低频率数据称为降采样,将地频率数据转化为高频率数据称为升采样

例如:

t.resample("M").mean()  # 按月重采样后计算每月平均值

t.reesample("10D").count()  # 按10天从采样后计算每10天的总数

(14)对时间段的处理

在前面讲到的DatetimeIndex可以理解为对时间戳的处理,这一部分要说一下PeriodIndex,是对一个时间段的处理。如果数据中保存的时间不是一个完整的数据,而是分别存为了年、月、日,可以用下面的方法转换为时间类型进行处理:

period = pd.PeriodIndex(year, month, day, hour, freq)

前四个参数传入对应的列,再根据freq参数生成一个时间段。

对于时间段的重采样,可以按下面的方法进行:

data = df.set_index(period).resample("10D")

你可能感兴趣的:(Python数据分析基础)