0%

关于python处理数据和使用matplotlib画图的一些总结

最近公司跟同济合作弄了一个气象数据分析模型,后续需要我做一些对excel数据的处理、绘制一些特定的图表(例如饼状图,雷达图,箱型图,以及最复杂的是一个颜色块阵图)。下面是我对一些用到的工具的总结,譬如画图的库matplotlib,Excel与Python串联的工具pandas,以及Python中强大的数学库Numpy。

使用Pandas从excel导入导出、处理数据

Pandas是一个强大的分析结构化数据的工具集;它的使用基础是Numpy(提供高性能的矩阵运算);用于数据挖掘和数据分析,同时也提供数据清洗功能。他有两个比较实用的小工具:DataFrame和Series。他们的内容其实很多,我只总结我用到的,有兴趣的可以自己去翻文档。

利器之一:DataFrame

DataFrame是Pandas中的一个表格型的数据结构,包含有一组有序的列,每列可以是不同的值类型(数值、字符串、布尔型等),DataFrame即有行索引也有列索引,可以被看做是由Series组成的字典。

从excel导入数据
1
2
3
4
import pandas as pd

filepath = '北京.xlsx'
df = pd.read_excel(filepath, sheet_name="逐时气象参数", skiprows=1)

上述代码会将”北京.xlsx”这个文件里的内容全部读入DataFrame以便后续的处理,我使用skiprows=1是因为从气象网站上拉下来的源数据第一行是空行,所以我把它去除掉了。pd.read_excel()这个方法的参数可以有20多个,有兴趣的自行查看。值得注意的是这时候的dataframe与初始excel表几乎一样,有一个区别是dataframe会在第一列添加从0依次递增的索引,类似于下图:

创建新的dataframe

很多时候我们处理完excel数据时候会出现新的列,或者新的分析结果需要插入到excel中,这时候就需要在python中使用DataFrame来保存你处理完的数据,例如我用了字典来存储城市的数据,字典内容如下:

1
2
3
4
5
{'上海': [1.61, 10.65, 6.5, 6.28, 5.09, 7.41], 
'乐山': [0.0, 9.08, 4.95, 4.74, 3.44, 6.24],
'东方': [2.79, 9.77, 6.5, 6.35, 5.44, 7.22],
'盐城': [2.42, 10.89, 7.18, 6.92, 5.7, 7.9],
'丽江': [0, 0, 0.0, 0.0, 0.0, 0.0]}

想要把它存入excel,并新建一个名为”除湿供冷”的表单,只需要

1
2
3
4
df = pd.DataFrame(box_dict)
write = pd.ExcelWriter('城市数据.xlsx')
df.to_excel(write, sheet_name='除湿供冷', index=False)
write.save()

注:如果你想放入excel的不是数据而是图片,使用以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
import xlwings as xw
import pandas as pd
from PIL import Image

wb = xw.Book('citymap.xlsx')
# 在wb中新建一张新的sheet.可以指定位置
sheet = wb.sheets.add(name=cityname, before=None, after=None)
im = Image.open(filepath)
width, height = im.size[0], im.size[1]
sheet.pictures.add(filepath, left=sheet.range('D5').left, top=sheet.range('D5').top,width=width,height=height)
wb.save()
wb.close()

利器之二:Series

它是一种类似于一维数组的对象,是由一组数据(各种NumPy数据类型)以及一组与之相关的数据标签(即索引)组成。仅由一组数据也可产生简单的Series对象。以前我使用过这个,但这次用的不多就不做过多介绍了,想了解的自行查阅文档。

关于图表上显示不出中文的问题

运行报错内容大致如下:

绘制出来的图像是这样的:
这个问题的原因是你的matplotlib(以下简称plt)找不到字体,需要自行设置一下,网上大部分解决方法是:
1
plt.rcParams['font.sans-serif']=['SimHei']

但是我自己的macbook pro并不带SimHei这个字体,而且我也不想去下载字体之后添加进去,折腾好久发现python还是自带可以显示中文的字体的,输入如下代码可以查看系统可用字体

1
2
3
4
from matplotlib.font_manager import FontManager
fm = FontManager()
mat_fonts = set(f.name for f in fm.ttflist)
print(mat_fonts)

上面的代码大家有兴趣可以自己去尝试着玩一下,下面才是重点,我是用的是下面这个字体就能正常显示中文了

1
2
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号

正确显示的饼状图如下:

绘制颜色块阵图

其实很多plt可以画出来的图,excel里也可以画出来,只不过在对于源数据的处理上可能使用excel本身的话,没有像python这么方便,这里的这个块阵图excel也可以做得到,只是数据不好处理而且不太美观。首先说一下我大概要画什么,需求是这样的,举个简单例子:假设有一个5*3共15个格子的方格,每个方格里有1-9的数字,我们要根据数字的不同,给他填充上对应的颜色,比如1对应蓝色,2对应红色。如果你进入到Matplotlib官网,它里面有类似的,比如”彩色网格”:

只不过类似这种的,官网上的方案只有依靠pcolormesh 或者 cmap(一般就是一个色阶)来完成,无法自定义数字对应颜色的要求,下面介绍我发现的两种方法,数据处理这里不赘述了,一些巧妙地方法后面介绍,默认这里的输入数据就是一个python矩阵(我的数据是365天*24小时=8760小时的矩阵)。

方法1:使用Image直接生成

首先将每个格子的数字,拆分成[r,g,b]三原色数组

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
# 填充地图数组的颜色通道
for i in range(365):
for j in range(24):
value = int(map_data[i][j])
if value == 0: # 如果地图值为0,显示黑色
map_data[i][j] = [0, 0, 0]
else: # 将流量低的区域根据区域号码来填充RGB三通道
r = (value % 255) * 50 % 255 # 取模乘以50再取模作为R通道
g = (value // 255) * 50 + 100 # 取整加100作为G通道的值
b = value % 255 # 取模作为B通道的值
if value == 1:
map_data[i][j] = [117,21,21] # 棕色
elif value == 2:
map_data[i][j] = [255, 109, 109] # 红色
elif value == 3:
map_data[i][j] = [231, 103, 174] # 紫色
elif value == 4:
map_data[i][j] = [156, 156, 156] # 灰色
elif value == 5:
map_data[i][j] = [255, 192, 203] # 粉色
elif value == 6:
map_data[i][j] = [255, 251, 153] # 黄色
elif value == 7:
map_data[i][j] = [93,201,149] # 青色
elif value == 8:
map_data[i][j] = [8,33,170] # 蓝色
elif value == 9:
map_data[i][j] = [60, 123, 36] # 绿色

然后利用Image将转换过后的颜色矩阵,绘制出来:

1
2
3
4
5
6
7
# 输出png图像
map_data = np.array(map_data)
map_data = np.asarray(map_data, np.uint8)
pic = Image.fromarray(map_data)
# 将图片逆时针旋转90度
pic = pic.transpose(Image.ROTATE_90)
pic.save('city.png')

效果图如下:

方法2:使用matplotlib来完成

首先,定义一个颜色对应的矩阵:[数值,r,g,b]

1
2
3
4
5
6
7
8
9
ca = np.array([[1,117,21,21],
[2,255, 109, 109],
[3,231, 103, 174],
[4,156, 156, 156],
[5,255, 192, 203],
[6,255, 251, 153],
[7,93,201,149],
[8,8,33,170],
[9,60, 123, 36]])

然后将输入的矩阵,每个色块的数值变为[r/255,g/255,b/255],这样的格式才能给plt制图

1
2
3
4
5
6
7
u, ind = np.unique(a, return_inverse=True)
ind = a - 1
c = ca[ca[:,0].argsort()][:,1:]/255.
b = np.moveaxis(c[ind][:,:,np.newaxis],1,2).reshape((a.shape[0],a.shape[1],3))
fig, ax = plt.subplots()
plt.imshow(b)
plt.savefig('pic_name',dpi = 600)

上述代码的第一行中的ind,在矩阵规模小的时候我测试了没问题,但是可能是我的数据过大还是什么原因,处理的不正确,所以我下面手动纠正了一步ind=a-1,经过上述代码我就将我需要的矩阵填充上了我想要的颜色。

上面两个图本质上是一模一样的,只是下面的图我用了一些小技巧让他更加好看:
纵向拉伸:

因为365*24这样的数据画出来的图,按照每一个网格都是正方形计算,纵向会特别窄,所以我把源数据矩阵每一行复制了四次,用以模拟纵向拉伸,这样做的还有一点好处(相较于图片直接拉伸)是图像不会失真。复制四次代码使用numpy也是轻松解决(感叹一句numpy真nb!):

1
a = np.repeat(res,repeats = 4,axis=0)

res就是原来的矩阵,a就是按行复制4次之后的numpy矩阵

将365天的横坐标改成1-12月

需要做的是把每个月月中的那一天算出来是365天中的第几天,依次对应即可,纵坐标以此类推,因为放大了4倍,所以对应着除以4就可以,这里不赘述了。

1
2
3
4
5
6
month_starts = [14,45,73,104,134,165,195,226,257,287,318,348]
month_names = ['1月','2月','3月','4月','5月','6月',
'7月','8月','9月','10月','11月','12月']

ax.set_xticks(month_starts)
ax.set_xticklabels(month_names)
加上垂直于横坐标的参考线

如果不加这个参考线,我们看整个图的时候在对应上会有些吃力。这里我偷了个懒,参考线直接按照365天除以12个月平分了,而不是每个月精细地去标:

1
2
3
miloc = plt.MultipleLocator(365/12)
ax.xaxis.set_minor_locator(miloc)
plt.grid(which='minor',axis='x',linestyle='dashed', color='grey')

一些其他小技巧

上述基本就是这次的总结内容了,还有一些使用中碰到的小问题,可以用一些小技巧去解决。

饼状图加上图例以及控制图例的位置

使用ax.legend()即可

1
2
3
4
5
6
ax.legend(wedges, new_labels,
title="模式",
loc="center left",
bbox_to_anchor=(1.3, 0, 0.5, 1))

fig.subplots_adjust(right=0.7)

饼状图默认显示的是比例,如何显示数值,以及如何不显示数值为0的数据?

正常的饼状图按照我的数据如下图,应该有9个颜色,但是因为我这里两个模式的数据为0,所以将其去除了:

做法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
labels = ['供热+加湿', '供热', '供热+除湿', '加湿', '通风', '除湿', '供冷+加湿', '供冷', '供冷+除湿']
sizes = [time1, time2, time3, time4, time5, time6, time7, time8, time9]
sizes2 = np.array(sizes)
index = np.where(sizes2==0)[0]
index = index.tolist()
new_labels = [labels[i] for i in range(0, len(labels), 1) if i not in index] # 删除后的列表
new_time = [sizes[i] for i in range(0, len(sizes), 1) if i not in index] # 删除后的列表
colors = ['#751515', '#ff6d6d', '#e767ae', '#9c9c9c', '#ffc0cb', '#fffb99', '#5dc995', '#0821aa', '#3c7b24']
new_colors = [colors[i] for i in range(0, len(colors), 1) if i not in index]
def func(pct, allvals):
absolute = int(pct/100.*np.sum(allvals))
return "{:d}".format(absolute)

wedges, texts, c = ax.pie(new_time, autopct=lambda pct: func(pct, new_time),colors=new_colors,radius=1.2,labels=new_labels)

关于雷达图的画法:

写累了,直接上代码吧,哈哈哈哈,这里我做了个小处理是把数据放在了标签的下面:

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
time = [time1,time2,time3,time4]
name = ['供冷\n'+str(time1), '供热\n'+str(time2), '除湿\n'+str(time3),'加湿\n'+str(time4)] # 标签

fig = plt.figure()
ax = fig.add_subplot(111, projection='polar') #创建极坐标的Axes对象
max,min = max(time),min(time)

## 提取数值信息和标签信息
data=np.array(time)
label=np.array(name)
n = len(label)

# print(data)
theta = np.linspace(0, 2*np.pi, len(data), endpoint=False) #计算区间角度
thetas = np.concatenate((theta, [theta[0]])) #添加第一个数据,实现闭合
data = np.concatenate((data, [data[0]])) #添加第一个数据,实现闭合


ax.set_thetagrids(thetas*180/np.pi, label) #设置网格标签,单位转化成度数

ax.set_theta_zero_location('N') #设置极坐标0°位置
ax.set_rlim(0, max) #设置显示的极径范围
ax.fill(thetas, data, facecolor='b', alpha=0.15) #填充颜色


ax.tick_params(pad=12, grid_color='k', grid_alpha=0.2, grid_linestyle=(0, (5, 5)))
# 取消标签显示
ax.tick_params('y', labelleft=False) # 取消left即可(top,bottom,right)
# 获取输入文件中的城市名字
a = filepath.split('.')[0]
str = a+'强度雷达图'

fig.text(0.5, 0.965, str,horizontalalignment='center', color='black', weight='bold',size='large')
plt.tight_layout()
plt.savefig(a)
plt.show()

效果图如下: