首页 > 编程语言 >Python pandas 数据清洗与数据绘图实战

Python pandas 数据清洗与数据绘图实战

时间:2023-04-22 19:36:31浏览次数:37  
标签:Python NaN element station 绘图 txt 数据 pandas


1、Python数据探索

Python已成为数据科学的主要语言之一,并继续在数据科学领域不断壮大。如前所述,就原始性能而言,Python并不总是速度最快的语言。但是有些数据处理库(如NumPy)主要用C语言编写,并且经过大量优化,以至于速度不再是问题。

此外,对可读性和可访问性的考虑往往超过了纯粹的速度需求,最大程度地节省开发人员的时间往往更为重要。Python具有较好的可读性和可访问性,并且无论是单独使用还是与Python社区开发的工具相结合,都是极其强大的数据操作和探索工具。

数十年来,电子表格一直是即兴(ad-hoc)数据处理的首选工具。熟悉电子表格的人能够发挥出着实惊人的技巧,可以组合有关联的不同数据集、数据透视表,可以用查找表链接数据集等。尽管每天到处都有人用电子表格完成了大量工作,但它确实存在局限性,Python就能有助于超越这些限制。

之前已经提到过的一个限制是,大多数电子表格软件都有行数限制,目前大约是100万行,这对于许多数据集来说是不够用的。另一个限制就是电子表格本身的寓意。电子表格是二维网格,就是行和列,顶多也就是一堆的网格,这限制了复杂数据的操作与思维方式。

有了Python,就可以绕开电子表格的限制编写代码,按照希望的方式操作数据。可以用无限灵活的方式组合Python数据结构,如列表、元组、集合和字典,或者可以创建自己的类,完全根据需要将数据和行为打包在一起。

2、Jupyter记事本

这或许算是最引人注目的Python数据探索工具之一,不会增加语言本身的功能,但会改变Python与数据的交互方式。

Jupyter记事本是一种通过Web浏览器访问Python的实用方式,也更容易实现较好的显示效果。

Jupyter记事本是个Web应用程序,能够创建和共享包含实时代码、方程式、可视化效果和说明文本的文档。虽然它现在已能支持其他几种语言,但起源与IPython有关,IPython是科学计算社区开发的Python shell替代品。

正是因为能在Web浏览器中与Jupyter进行交互,才使它成为一个便利而强大的工具。它允许混合使用文本和代码,还允许以交互方式修改和执行代码。不仅可以运行和修改大段代码,还可以保存记事本并与他人分享。

了解Jupyter记事本功能的最佳方式,就是开始使用。在机器上本地运行Jupyter进程相当容易,或者访问在线版本也可以。

以下Jupyter运行方式中,介绍了一些运行选项。

在线Jupyter:访问Jupyter的在线实例是最简单的入门方式之一。目前,Jupyter项目组(Jupyter的支持社区)在Jupyter官方网站上提供免费的记事本。这里还可以找到其他语言的演示记事本和内核。

本地Jupyter:尽管在线实例的使用非常方便,但在自己的计算机上设置Jupyter实例也没那么复杂。通常对于本地版本,要让浏览器指向localhost:8888。

如果用的是Docker,那么可有几种容器供选择。

如果要运行数据科学记事本容器,可以采用如下命令:

docker run -it --rm -p 8888:8888 jupyter/datascience-notebook

如果愿意在自己的系统中直接运行,在虚拟环境中安装和运行Jupyter也是很简单的。

macOS和Linux系统:首先打开命令窗口,输入以下命令:

> python3 -m venv jupyter
> cd jupyter
> source bin/activate
> pip install jupyter
> jupyter-notebook

Windows系统:

> python3 -m venv jupyter
> cd jupyter
> Scripts/bin/activate
> pip install jupyter
> Scripts/jupyter-notebook

最后一条命令应该会把Jupyter记事本Web应用程序运行起来,并打开浏览器窗口指向它。

Jupyter安装完毕,在浏览器中运行并打开,然后就需要启动Python内核了。Jupyter有一个好处,就是能够同时运行多个内核。可以为不同版本的Python以及其他语言(如R、Julia甚至Ruby)运行各自的内核。

如下图所示,启动内核很简单,只要点击new按钮并选择Python 3即可。

Python pandas 数据清洗与数据绘图实战_python

有了正在运行的内核,就可以开始输入并运行Python代码了。大家马上就会发现,这里与普通的Python命令shell有点不一样。这里看不到标准Python shell中的“>>>”提示符,按下Enter键也只会在单元格(cell)内添加新行。

如下图所示,要想执行单元格中的代码,可以选择Cell→Run Cells,也可以在按钮工具栏中点击下箭头右侧的Run按钮,或使用组合键Alt+Enter也行。在用过几次Jupyter记事本之后,很可能使用Alt+Enter组合键就比较顺手了。

Python pandas 数据清洗与数据绘图实战_数据_02

在新记事本的第一个单元格中输入一些代码或表达式,然后按下Alt+Enter键,就可以进行测试了。 

正如所见,所有输出结果都将马上在单元格下方显示出来,同时会创建一个新的单元格,准备接受下一次输入。另请注意,每个执行过的单元格都按执行顺序编了号。

3、pandas工具

在探索和操作数据的过程中,需要执行很多的常见操作,例如,将数据加载到列表或字典中、清洗数据并过滤数据。这里的大多数操作经常需要重复执行,且必须在标准的模式下执行,往往既简单又乏味。以上都是上述数据操作应该自动执行的有力理由,这种想法并不少见。

pandas就是一种用Python处理数据的当红标准(now-standard)工具之一,创造出来的目的就是将无聊繁重的数据集处理工作自动化。

1. 为什么要选用pandas

创建pandas就是为了能简化表格或关系式数据的操作和分析,它给出了一种数据存放的标准框架,并为常见操作提供了方便的工具。因此它几乎更像是Python的扩展,而不只是一个简单的库,完全改变了数据交互的方式。

好的方面就是,在了解了pandas的工作方式后,可以完成一些惊人的工作并节省大量时间。不过学习如何最大限度地用好pandas,确实需要花费很多时间。与很多其他工具一样,如果能按照设计目标去使用,那么pandas确实很优秀。

下面的简单示例,应该能对pandas是否为合适工具给出一个大致的说明。

2. pandas的安装

用pip就能轻松安装pandas。因为pandas常常会连同matplotlib来画图,所以两者可以同时安装这两个工具,在Jupyter虚拟环境的命令行中,可以用以下命令安装:

> pip install pandas matplotlib

在Jupyter记事本的单元格中,可以用以下命令安装:

In [ ]: !pip install pandas matplotlib

在使用pandas时,执行以下3行命令可以简化很多操作:

%matplotlib inline
import pandas as pd
import numpy as np

第一行是Jupyter的“魔法”(magic)功能,使得matplotlib能够在代码所在的单元格中绘制数据,这个功能非常有用。第二行导入pandas并指定别名为pd,pandas用户往往采用这个别名,也简化了代码的输入。

最后一行还导入了numpy库。虽然pandas对numpy的依赖并不少,但在以下示例中不会显式地用到numpy,不管怎样养成导入它的习惯都是有道理的。

3. Data Frame

由pandas得到的一种基本结构就是Data Frame。Data Frame是一个二维网格,类似于关系数据库的表,但是位于内存中。Data Frame很容易创建,只要给出一些数据即可。为了让事情尽量简单,就给出3x3网格的数字作为第一个例子吧。

在Python中,这种网格就是列表的列表:

grid = [[1,2,3], [4,5,6], [7,8,9]] 
print(grid)

[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

遗憾的是,Python中的网格看起来不像是网格,除非多加一些显示格式的处理。下面看看同样的网格数据用pandas的Data Frame可以做些什么:

import pandas as pd
df = pd.DataFrame(grid)
print(df)

    0  1  2
 0  1  2  3
 1  4  5  6
 2  7  8  9

代码非常简单,只要把网格转换为Data Frame即可。显示的结果比较像是网格,现在有了行号和列号。

当然,要记住列号往往很麻烦,所以可以显示列名:

df = pd.DataFrame(grid, columns=["one", "two", "three"] )
print(df)
     one  two  three
  0    1    2      3
  1    4    5      6
  2    7    8      9

或许大家很想知道列的命名是否有用处,但至少列的名称可以与另一个pandas技巧一起使用,即能按名称选取列。

例如,只想获取"two"列的内容,就非常简单:

print(df["two"])
0    2
1    5
2    8
Name: two, dtype: int64

与Python相比,这里已经节省了不少时间。在Python中如果只想获取网格的第二列,需要用到列表推导,同时还要记得采用从零开始的索引,并且仍然无法获得美观的输出:

print([x[1] for x in grid])
[2, 5, 8]

可以像对推导所得的列表那样,轻松地对Data Frame列值进行循环遍历。

for x in df["two"]:
    print(x)
2
5
8

这是个好的开始,但还能做得更好。利用双括号包围的数据列的列表,就能得到Data Frame的子集。

下面获取的不是中间列,而是Data Frame的第一列和最后一列,形成另一个Data Frame:

edges = df[["one", "three"]]
print(edges)
   one  three
0    1      3
1    4      6
2    7      9

Data Frame还带有几个方法,可以对Data Frame中的每一项应用相同的操作和参数。

如果要将Data Frame每条边上的数据项加2,可以用add()方法:

print(edges.add(2))
   one  three
0    3      5
1    6      8
2    9     11

采用列表推导和/或嵌套循环可以得到相同的结果,但那些技术用起来不大方便。很容易看出,Data Frame的这些功能是如何让编程生涯过得更加轻松的。

4、数据清洗

前面讨论了Python清洗数据的几种方案。现在已经有了pandas的加入,那就来展示一下利用pandas的功能来清洗数据的例子。

在给出以下操作时,还会涉及在普通Python中完成相同操作的方案,既为了说明采用pandas的方式有何不同,也是为了演示为什么pandas并不适用于所有使用场景或用户。

1. 用pandas加载并保存数据

pandas有一系列令人印象深刻的方法,用于加载来自各种来源的数据。pandas支持多种文件格式,包括固定宽度和带分隔符的文本文件、电子表格、JSON、XML和HTML,但也可以从SQL数据库、Google BiqQuery、HDF甚至剪贴板中读取数据。

必须要清楚的是,这里有很多操作其实并不属于pandas本身的功能,pandas有赖于安装的其他库来处理这些操作,例如,SQL数据库的读取就是用SQLAlchemy完成的。当出现问题时,这种区分就很重要了。需要修复的往往是pandas之外的问题,只要把底层库的问题解决即可。

用read_json()方法读取JSON文件就很简单:

mars = pd.read_json("mars_data_01.json")

以上代码会给出下面这个Data Frame:

report
abs_humidity                         None
atmo_opacity                        Sunny
ls                                    296
max_temp                               -1
max_temp_fahrenheit                  30.2
min_temp                              -72
min_temp_fahrenheit                 -97.6
pressure                              869
pressure_string                    Higher
season                           Month 10
sol                                  1576
sunrise              2017-01-11T12:31:00Z
sunset               2017-01-12T00:46:00Z
terrestrial_date               2017-01-11
wind_direction                         --
wind_speed                           None

再举一个pandas轻松读取数据的例子,温度数据CSV文件和火星天气数据JSON文件中加载一些数据。

第一种情况用到了read_csv()方法:

temp = pd.read_csv("temp_data_01.csv")

          4      5    6     7     8      9    10    11    12     13     14 \    ⇽---  请注意表头末尾的\表示表格太长了,一行显示不下,剩余的列会在下面继续显示

0  1979/01/01  17.48  994   6.0  30.5   2.89  994 -13.6  15.8    NaN    0   
1  1979/01/02   4.64  994  -6.4  15.8  -9.03  994 -23.6   6.6    NaN    0   
2  1979/01/03  11.05  994  -0.7  24.7  -2.17  994 -18.3  12.9    NaN    0   
3  1979/01/04   9.51  994   0.2  27.6  -0.43  994 -16.3  16.3    NaN    0   
4  1979/05/15  68.42  994  61.0  75.1  51.30  994  43.3  57.0    NaN    0   
5  1979/05/16  70.29  994  63.4  73.5  48.09  994  41.1  53.0    NaN    0   
6  1979/05/17  75.34  994  64.0  80.5  50.84  994  44.3  55.7  82.60    2   
7  1979/05/18  79.13  994  75.5  82.1  55.68  994  50.0  61.1  81.42  349   
8  1979/05/19  74.94  994  66.9  83.1  58.59  994  50.9  63.2  82.87   78   

     15    16      17  
0   NaN   NaN  0.0000  
1   NaN   NaN  0.0000  
2   NaN   NaN  0.0000  
3   NaN   NaN  0.0000  
4   NaN   NaN  0.0000  
5   NaN   NaN  0.0000  
6  82.4  82.8  0.0020  
7  80.2  83.4  0.3511  
8  81.6  85.2  0.0785

一步就能完成文件载入,这显然很有吸引力,看得出pandas在加载文件时没有发生问题。并且第一个空列已被转换为NaN,而不是数值。对于某些值,确实还会碰到和'Missing'同样的问题,事实上将这些'Missing'值转换为NaN可能确实合理:

temp = pd.read_csv("temp_data_01.csv", na_values=['Missing'])

加上了na_values参数之后,就可以控制在加载时要把哪些值转换为NaN。

这里是把字符串'Missing'加上了,因此Data Frame的数据行将从:

NaN  Illinois  17  Jan 01, 1979  1979/01/01  17.48  994  6.0  30.5  2.89994
     -13.6  15.8  Missing  0  Missing  Missing  0.00%

转换为:

NaN  Illinois  17  Jan 01, 1979  1979/01/01  17.48  994  6.0  30.5  2.89994
     -13.6  15.8  NaN0  NaN  NaN  0.00%

假如有这么一个数据文件,由于未知的原因,“没有数据”的表示方式会有很多种,如“NA”“N/A”“?”“-”等,那么以上转换技术就特别有用了。

要想处理这种情况,可以对数据进行检查,找出用到的替换字符,然后带上na_values参数把所有变体都标准化为NaN,重新加载数据。

如果要保存Data Frame的内容,pandas的Data Frame同样提供了大批的方法。对于简单的网格Data Frame,写入数据的方式有很多种。

df.to_csv("df_out.csv", index=False)     ⇽---  index设为False表示不写入行索引

以上代码写入的文件将如下所示:

one,two,three
1,2,3
4,5,6
7,8,9

同理,可以将数据网格转换为JSON对象或直接写入文件:

df.to_json()     ⇽---  如果给出文件路径做参数,就会把JSON数据写入该文件,而不会再返回数据
'{"one":{"0":1,"1":4,"2":7},"two":{"0":2,"1":5,"2":8},"three":{"0":3,"1":6,"2
      ":9}}'

2. 用Data Frame进行数据清洗

在加载时将一组特定值转换为NaN,是一种非常简单的数据清洗过程,pandas做起来不费吹灰之力。它的能力远不止这些,Data Frame支持多种操作,以便减少数据清洗的麻烦。为了查看这些功能,请重新打开温度CSV文件,但这次不用标题来命名列,而用带names参数的range()函数给各列指定一个数字,这样就更容易引用了。

还记得之前的示例吧,每行的第一个字段Notes是空的,载入成了NaN值。虽然能够忽略该列,但如果让它消失则会更简单。还是可以用到range()函数,这次从1开始,通知pandas载入除第一列之外的所有列。

但如果不关心long-form date字段,那就可以从列4开始加载,以使得数据管理更加简单:

temp = pd.read_csv("temp_data_01.csv", na_values=['Missing'], header=0,     ⇽---  header为0将禁止读入标题并用作列标签
     names=range(18), usecols=range(4,18)) 
print(temp)  

           4      5    6     7     8      9    10    11    12     13   14  \
0  1979/01/01  17.48  994   6.0  30.5   2.89  994 -13.6  15.8    NaN    0   
1  1979/01/02   4.64  994  -6.4  15.8  -9.03  994 -23.6   6.6    NaN    0   
2  1979/01/03  11.05  994  -0.7  24.7  -2.17  994 -18.3  12.9    NaN    0   
3  1979/01/04   9.51  994   0.2  27.6  -0.43  994 -16.3  16.3    NaN    0   
4  1979/05/15  68.42  994  61.0  75.1  51.30  994  43.3  57.0    NaN    0   
5  1979/05/16  70.29  994  63.4  73.5  48.09  994  41.1  53.0    NaN    0   
6  1979/05/17  75.34  994  64.0  80.5  50.84  994  44.3  55.7  82.60    2   
7  1979/05/18  79.13  994  75.5  82.1  55.68  994  50.0  61.1  81.42  349   
8  1979/05/19  74.94  994  66.9  83.1  58.59  994  50.9  63.2  82.87   78   

     15    16      17  
0   NaN   NaN   0.00%  
1   NaN   NaN   0.00%  
2   NaN   NaN   0.00%  
3   NaN   NaN   0.00%  
4   NaN   NaN   0.00%  
5   NaN   NaN   0.00%  
6  82.4  82.8   0.20%  
7  80.2  83.4  35.11%  
8  81.6  85.2   7.85%

现在的Data Frame中只包含可能需要处理的列。但还有一个问题,最后一列,也就是给出热指数覆盖百分比的列,仍然是以百分号结尾的字符串,而不是实际的百分数。

不妨查看一下第17列第一行的值,问题很明显:

temp[17][0]
'0.00%'

如果要修复该问题,需要做两件事情,先删除数据末尾的“%”,然后从字符串转换为数字。还可选择一步操作,如果要把结果百分数表示为分数,则还需除以100。

第一步很简单,因为pandas能够用一条命令在某一列上重复执行操作:

temp[17] = temp[17].str.strip("%")
temp[17][0]
'0.00'

以上代码将读取一列并对其调用字符串strip()操作,以便删除尾部的“%”。然后再查看该列的第一个值(或其他任一值),就会发现烦人的百分号消失了。还有一点值得注意,用replace("%", "")之类的其他操作,也可以获得相同的结果。

第二步操作是将字符串转换为数值。同样,pandas能够用一条命令执行该操作:

temp[17] = pd.to_numeric(temp[17]) 
temp[17][0]
0.0

现在,第17列的值已经变成数字了,如果需要的话,就可以用div()方法完成转换为分数的工作了:

temp[17] = temp[17].div(100)
temp[17]

0    0.0000
1    0.0000
2    0.0000
3    0.0000
4    0.0000
5    0.0000
6    0.0020
7    0.3511
8    0.0785
Name: 17, dtype: float64

其实只用一条命令获得同样的结果也是完全可能的,只要把3步操作串接起来即可:

temp[17] = pd.to_numeric(temp[17].str.strip("%")).div(100)

上述例子非常简单,但已大致展示了pandas为数据清洗带来的便利。pandas有各种各样的转换数据操作,以及使用自定义函数的能力,所以要想出一种无法用pandas简化数据清洗的场景,也是很难的。

pandas可选功能的数量几乎是天下无双,还具备各种各样的教程和视频,并且pandas官方网站上的文档也十分优秀。

5、数据聚合

上述示例可能已对pandas的众多可选功能做了一定的展示,只需几条命令就能对数据执行相当复杂的操作。正如所料,这类功能也可用于数据的聚合。

1. Data Frame的合并

在数据处理过程中,往往有关联两个数据集的需求。假设某个文件中包含了销售团队成员每月销售电话的数量,而在另一个文件中包含了他们所属地区的美元销售额:

calls = pd.read_csv("sales_calls.csv")
print(calls)

   Team member  Territory  Month  Calls
0        Jorge          3      1    107
1        Jorge          3      2     88
2        Jorge          3      3     84
3        Jorge          3      4    113
4          Ana          1      1     91
5          Ana          1      2    129
6          Ana          1      3     96
7          Ana          1      4    128
8          Ali          2      1    120
9          Ali          2      2     85
10         Ali          2      3     87
11         Ali          2      4     87

revenue = pd.read_csv("sales_revenue.csv")
print(revenue)

    Territory  Month  Amount
0           1      1   54228
1           1      2   61640
2           1      3   43491
3           1      4   52173
4           2      1   36061
5           2      2   44957
6           2      3   35058
7           2      4   33855
8           3      1   50876
9           3      2   57682
10          3      3   53689
11          3      4   49173

将收入和团队成员的活跃度联系起来,显然非常有用。虽然这两个文件非常简单,但要用普通Python代码将它们合并起来,却并不是毫不费力。pandas就提供了合并两个Data Frame的函数:

calls_revenue = pd.merge(calls, revenue, on=['Territory', 'Month'])

merge函数会根据给定的列连接两个Data Frame,并创建一个新的Data Frame。merge函数的工作方式与关系数据库的join操作类似,给出的是一张数据表,其中组合了来自两个文件的数据列:

print(calls_revenue)

   Team member  Territory  Month  Calls  Amount
0        Jorge          3      1    107   50876
1        Jorge          3      2     88   57682
2        Jorge          3      3     84   53689
3        Jorge          3      4    113   49173
4          Ana          1      1     91   54228
5          Ana          1      2    129   61640
6          Ana          1      3     96   43491
7          Ana          1      4    128   52173
8          Ali          2      1    120   36061
9          Ali          2      2     85   44957
10         Ali          2      3     87   35058
11         Ali          2      4     87   33855

这里两个字段的行存在一一对应关系,但merge函数还可以进行一对多和多对多的连接,以及左连接和右连接。

2. 数据选取

根据某些条件选取或过滤Data Frame中的行,也是很有用的。在销售数据的示例中,可能只想查看第三区的数据,这也很容易:

print(calls_revenue[calls_revenue.Territory==3])

  Team member  Territory  Month  Calls  Amount
0       Jorge          3      1    107   50876
1       Jorge          3      2     88   57682
2       Jorge          3      3     84   53689
3       Jorge          3      4    113   49173

在以上示例中,仅需采用表达式calls_revenue.Territory == 3作为Data Frame的索引,就实现了选取地区编号等于3的行。从普通Python代码的角度来看,这种写法毫无意义,也是非法的。但对于pandas的Data Frame而言,这种写法能够生效,而且让表达式简洁许多。

当然,还能使用更加复杂的表达式。如果只想选取每个电话的收入超过500美元的行,则可以换成以下表达式:

print(calls_revenue[calls_revenue.Amount/calls_revenue.Calls>500])

  Team member  Territory   Month   Calls    Amount
1       Jorge          3       2      88     57682
2       Jorge          3       3      84     53689
4         Ana          1       1      91     54228
9         Ali          2       2      85     44957

再进一步,甚至还可以在Data Frame中把每个电话的收入计算出来并添加为数据列,操作是类似的:

calls_revenue['Call_Amount'] = calls_revenue.Amount/calls_revenue.Calls
print(calls_revenue)

   Team member  Territory  Month  Calls  Amount  Call_Amount
0        Jorge          3      1    107   50876   475.476636
1        Jorge          3      2     88   57682   655.477273
2        Jorge          3      3     84   53689   639.154762
3        Jorge          3      4    113   49173   435.159292
4          Ana          1      1     91   54228   595.912088
5          Ana          1      2    129   61640   477.829457
6          Ana          1      3     96   43491   453.031250
7          Ana          1      4    128   52173   407.601562
8          Ali          2      1    120   36061   300.508333
9          Ali          2      2     85   44957   528.905882
10         Ali          2      3     87   35058   402.965517
11         Ali          2      4     87   33855   389.137931

注意,pandas的内建逻辑再次取代了普通Python那些烦琐很多的代码结构。

3. 分组与聚合

正如所料,panda还拥有大量用于合计和聚合数据的工具。特别是计算某列的合计值、平均值、中位数、最小值、最大值,只要对明确给出名称的列调用方法即可:

print(calls_revenue.Calls.sum())
print(calls_revenue.Calls.mean())
print(calls_revenue.Calls.median())
print(calls_revenue.Calls.max())
print(calls_revenue.Calls.min())

1215
101.25
93.5
129
84

例如,要获得每个电话的收入高于中位数的所有行,就可以结合使用聚合与选取操作:

print(calls_revenue.Call_Amount.median())
print(calls_revenue[calls_revenue.Call_Amount >= 
      calls_revenue.Call_Amount.median()])

464.2539427570093
  Team member  Territory  Month    Calls   Amount  Call_Amount
0       Jorge          3      1      107    50876   475.476636
1       Jorge          3      2       88    57682   655.477273
2       Jorge          3      3       84    53689   639.154762
4         Ana          1      1       91    54228   595.912088
5         Ana          1      2      129    61640   477.829457
9         Ali          2      2       85    44957   528.905882

除能挑出合计数之外,基于其他列对数据进行分组往往也很有用。在以下的简单示例中,用groupby()方法对数据进行分组。

例如,可能想知道按月或按地区汇总的电话数和收入,这时就可对这些字段调用Data Frame的groupby()方法:

print(calls_revenue[['Month', 'Calls', 'Amount']].groupby(['Month']).sum())

       Calls  Amount
Month               
1        318  141165
2        302  164279
3        267  132238
4        328  135201

print(calls_revenue[['Territory', 'Calls', 
     'Amount']].groupby(['Territory']).sum())

           Calls  Amount
Territory               
1            444  211532
2            379  149931
3            392  211420

在以上两种情况下,分别选取需要执行聚合操作的列,按照其中一列的值进行分组,这里还对每组值进行求和。

上述示例也都很简单,但也演示了用pandas操作和选取数据时的一些可用功能。如果这些想法符合需求,就可以研究pandas官方网站中的pandas文档,以便了解更多信息。

6、用matplotlib绘图

pandas还有一个非常吸引人的功能,就是能非常容易地将Data Frame中的数据制成图表。虽然在Python和Jupyter记事本中有很多数据绘图包可选,但是pandas可以直接从Data Frame中使用matplotlib。

回想一下,在启动Jupyter会话时,最初给出的命令中就有一条Jupyter的“魔法”指令,启用matplotlib用于内联绘图:

%matplotlib inline

因为已经具备了绘图能力,下面就来看看如何绘制一些数据。继续采用之前的销售示例,如果想按地区绘制季度平均销售额,只需加上“.plot.bar()”就可以在记事本中得到图表了:

calls_revenue[['Territory', 'Calls']].groupby(['Territory']).sum().plot.bar()

Python pandas 数据清洗与数据绘图实战_数据分析_03

其他还有多种图形可供选择。单独的plot()或.plot.line()将创建折线图,.plot.pie()将创建饼图,等等。

正是由于pandas和matplotlib的组合,在Jupyter记事本中的数据绘图就变得相当容易了。不过还是得注意,虽然这种绘图很简单,但有很多地方并没有做得十分出色。 

上述例子只是演示了pandas的一小部分工具,提供数据清洗、探索和操作功能。pandas是一个出色的工具集,在符合其设计的领域表现优异。但这并不意味着pandas是适用于所有情形或所有人的工具。

选用普通Python或其他工具的理由有很多。首先,如前所述,为了充分利用pandas必须经过学习,在某些方面就像在学习另一门语言,大家可能没有足够的时间或意愿去完成。此外,pandas可能无法在所有生产环境都有理想的表现,特别是针对非常大的数据集。这些大型数据集对数学运算的方式要求不高,或者数据难以变成pandas最擅长处理的格式。例如,大量产品信息的修改可能就不会从pandas获益太多,交易数据流的基本处理也是一样。

应该根据手头的问题仔细选择工具,这才是关键所在。在很多情况下,pandas确实能在处理数据时让人倍感轻松。但在其他情况下,普通的Python可能会是最佳选择。

7、 pandas数据清理与数据绘图实战

全球温度变化是讨论很多的主题,但这些讨论都是基于全球范围的。下面假设要了解所在地区的气温变化情况。有一种方法就是获取所在地区的历史数据,处理这些数据并绘制成图表,以便能确切了解到底发生了什么变化。

以下案例研究是用Jupyter记事本完成的。

幸运的是,很多历史天气数据源都是可以免费获取的。Global Historical Climatology Network 那里有世界各地的数据。大家或许还能找到其他的数据源,其数据格式可能不一样,但这里讨论的步骤和处理过程应该能适用于任何数据集。

1. 数据的下载

第一步是要获取数据。http://www.epubit.com:8083/quickpythonbook/daily/上的每日历史天气数据存档中,包含了大量的数据。首先找出所需文件及其确切位置,然后进行下载。获取数据之后,就可以继续处理并最终显示出结果了。

数据文件要通过HTTPS协议访问,下载文件需要用到requests库。在命令提示符下执行pip install requests,即可获得requests库。

requests库安装完毕后,首先请获取readme.txt文件,该文件有助于找出所需数据文件的格式和位置:

# 导入requests库
import requests
# 获取readme.txt文件

r = requests.get('http://www.epubit.com:8083/quickpythonbook/daily/readme.txt')
readme = r.text

得到readme.txt文件后,应该查看一下:

print(readme)
README FILE FOR DAILY GLOBAL HISTORICAL CLIMATOLOGY NETWORK (GHCN-DAILY) 
Version 3.24

--------------------------------------------------------------------------------
How to cite:

Note that the GHCN-Daily dataset itself now has a DOI (Digital Object Identifier)
so it may be relevant to cite both the methods/overview journal article as well 
as the specific version of the dataset used.

The journal article describing GHCN-Daily is:
Menne, M.J., I. Durre, R.S. Vose, B.E. Gleason, and T.G. Houston, 2012: An overview 
of the Global Historical Climatology Network-Daily Database.  Journal of Atmospheric 
and Oceanic Technology, 29, 897-910, doi:10.1175/JTECH-D-11-00103.1.
To acknowledge the specific version of the dataset used, please cite:
Menne, M.J., I. Durre, B. Korzeniewski, S. McNeal, K. Thomas, X. Yin, S. Anthony, R. Ray,
R.S. Vose, B.E.Gleason, and T.G. Houston, 2012: Global Historical Climatology Network - 
Daily (GHCN-Daily), Version 3. [indicate subset used following decimal, 
e.g. Version 3.12]. 
NOAA National Climatic Data Center.

应该特别关注的是第II节,其中列出了文件的内容:

II. CONTENTS OF ftp://...

all:                  Directory with ".dly" files for all of GHCN-Daily
gsn:                  Directory with ".dly" files for the GCOS Surface Network 
                      (GSN)
hcn:                  Directory with ".dly" files for U.S. HCN
by_year:              Directory with GHCN Daily files parsed into yearly
                      subsets with observation times where available.  See the
              /by_year/readme.txt and 
              /by_year/ghcn-daily-by_year-format.rtf 
              files for further information
grid:                 Directory with the GHCN-Daily gridded dataset known 
                      as HadGHCND
papers:           Directory with pdf versions of journal articles relevant
                      to the GHCN-Daily dataset
figures:          Directory containing figures that summarize the inventory 
                      of GHCN-Daily station records

ghcnd-all.tar.gz:  TAR file of the GZIP-compressed files in the "all" directory
ghcnd-gsn.tar.gz:  TAR file of the GZIP-compressed "gsn" directory
ghcnd-hcn.tar.gz:  TAR file of the GZIP-compressed "hcn" directory

ghcnd-countries.txt:  List of country/area codes (FIPS) and names
ghcnd-inventory.txt:  File listing the periods of record for each station and 

                      element
ghcnd-stations.txt:   List of stations and their metadata (e.g., coordinates)
ghcnd-states.txt:     List of U.S. state and Canadian Province codes 
                      used in ghcnd-stations.txt
ghcnd-version.txt:    File that specifies the current version of GHCN Daily

readme.txt:           This file
status.txt:           Notes on the current status of GHCN-Daily

请查看一下这些可供下载的文件。ghcnd-inventory.txt文件列出了每个观测站的记录周期,这有助于找到一个合适的数据集。而ghcnd-stations.txt文件则列出了所有观测站,这有助于找到离所在地区最近的观测站。

因此首先要抓取这两个文件:

# 获取inventory和stations文件

r = requests.get('http://www.epubit.com:8083/quickpythonbook/daily/ghcnd-inventory.txt') 
inventory_txt = r.text
r = requests.get('http://www.epubit.com:8083/quickpythonbook/daily/ghcnd-stations.txt')
stations_txt = r.text

得到这两个文件后,可以存入本地磁盘中,这样在需要恢复原始数据时就不需要重新下载:

# 将inventory和stations文件存入磁盘,以备不时之需

with open("inventory.txt", "w") as inventory_file:
    inventory_file.write(inventory_txt)

with open("stations.txt", "w") as stations_file:
    stations_file.write(stations_txt)

下面查看一下inventory.txt文件。下面展示的是前137个字符:

print(inventory_txt[:137])
ACW00011604  17.1167  -61.7833 TMAX 1949 1949
ACW00011604  17.1167  -61.7833 TMIN 1949 1949
ACW00011604  17.1167  -61.7833 PRCP 1949 1949

如果查看readme.txt文件的第VII节,就能发现inventory.txt文件的格式说明:

VII. FORMAT OF "ghcnd-inventory.txt"

------------------------------
Variable   Columns   Type
------------------------------
ID            1-11   Character
LATITUDE     13-20   Real
LONGITUDE    22-30   Real
ELEMENT      32-35   Character
FIRSTYEAR    37-40   Integer
LASTYEAR     42-45   Integer
------------------------------

These variables have the following definitions:

ID         is the station identification code.  Please see "ghcnd-stations.txt"
           for a complete list of stations and their metadata.

LATITUDE   is the latitude of the station (in decimal degrees).

LONGITUDE  is the longitude of the station (in decimal degrees).

ELEMENT    is the element type.  See section III for a definition of elements.

FIRSTYEAR  is the first year of unflagged data for the given element.

LASTYEAR   is the last year of unflagged data for the given element.

根据上述说明,可以得知inventory中包含了要查找的工作站所需的大部分信息。利用纬度和经度可以找到离本地最近的观测站,然后可以用FIRSTYEAR和LASTYEAR字段查找记录时间较长的观测站。

现在只剩下一个问题了,即ELEMENT字段是什么意思。对此,readme.txt文件建议查看第III节。第III节将在后续详细介绍,其中能找到元素的说明,主要的几个列出如下:

ELEMENT    is the element type.   There are five core elements as well as a number
           of addition elements.

       The five core elements are:

           PRCP = Precipitation (tenths of mm)
           SNOW = Snowfall (mm)
           SNWD = Snow depth (mm)
           TMAX = Maximum temperature (tenths of degrees C)
           TMIN = Minimum temperature (tenths of degrees C)

就本例的目标而言,感兴趣的是元素TMAX和TMIN,也就是最高和最低温度,以十分之一摄氏度为单位。

2. 解析inventory数据

readme.txt文件给出了inventory.txt文件的内容说明,因此可以将数据解析为更加有用的格式。当然可以将解析后的inventory数据保存为列表或元组的列表。

但只要再多做一点工作,就可以用collections库中的namedtuple创建一个带有命名属性的自定义类:

# 解析为命名元组

# 用namedtuple创建自定义类Inventory
from collections import namedtuple
Inventory = namedtuple("Inventory", ['station', 'latitude', 'longitude',
     'element', 'start', 'end'])

自建的Inventory类用起来非常简单,只需用适当的值创建实例即可,以上是一行经过解析的inventory数据。

解析过程涉及两步操作。首先,要根据指定的字段大小取出每行的各个片段。查看readme文件中的字段说明就会发现,文件之间明显存在多余的空白,在提出解析方案时需要考虑到这些空白。本例中因为会指定每个片段的位置,所以多余的空白会被忽略。此外,由于字段STATION和ELEMENT的大小与其中存放的数值精确对应,因此不用考虑删除多余空白的问题。

第二步应该完美解决的操作,就是把纬度和经度值转换为浮点数,以及把开始和结束年份转换为整数。这可以放到数据清洗的后期去完成,其实只要有某一行出现数据不一致且找不到能正确转换的数值,也许就该停下来等待。

但本例的数据可以在解析步骤中进行转换处理,所以就在此时完成吧:

# 解析inventory数据,将值转换为浮点数和整数

inventory = [Inventory(x[0:11], float(x[12:20]), float(x[21:30]), x[31:35],
     int(x[36:40]), int(x[41:45])) 
             for x in inventory_txt.split("\n") if x.startswith("US")]

for line in inventory[:5]:
    print(line)
Inventory(station='US009052008', latitude=43.7333, longitude=-96.6333, 
    element='TMAX', start=2008, end=2016)
Inventory(station='US009052008', latitude=43.7333, longitude=-96.6333, 
    element='TMIN', start=2008, end=2016)
Inventory(station='US009052008', latitude=43.7333, longitude=-96.6333, 
    element='PRCP', start=2008, end=2016)
Inventory(station='US009052008', latitude=43.7333, longitude=-96.6333, 
    element='SNWD', start=2009, end=2016)
Inventory(station='US10RMHS145', latitude=40.5268, longitude=-105.1113, 
    element='PRCP', start=2004, end=2004)

3. 根据经纬度选择一个观测站

现在inventory已加载完毕,然后就可以用纬度和经度找到离当前位置最近的观测站,并根据开始和结束年份挑出具有最久温度记录的观测站。即使是面对第一行数据,也能发现需要考虑下面两件事情。

元素的类型有好几种,但这里只关心TMIN和TMAX,即最低和最高温度。

这里看到的第一个inventory文件的数据都不超过几年。如果要回顾历史,还得找到时间更久的温度数据。

为了能快速找出所需数据,可以用列表推导式来构建仅包含TMIN或TMAX元素的观测站inventory数据项子列表。

由于另一件要关心的事就是找到一个包含长期数据的观测站,所以在创建这个子列表时,还得确保开始年份是1920年之前,并且结束年份至少是2015年。这样就能只关注那些至少具备95年有效数据的观测站了:

inventory_temps = [x for x in inventory if x.element in ['TMIN', 'TMAX'] 
                   and x.end >= 2015 and x.start < 1920]
inventory_temps[:5]

[Inventory(station='USC00010252', latitude=31.3072, longitude=-86.5225, 
     element='TMAX', start=1912, end=2017),
 Inventory(station='USC00010252', latitude=31.3072, longitude=-86.5225, 
     element='TMIN', start=1912, end=2017),
 Inventory(station='USC00010583', latitude=30.8839, longitude=-87.7853, 
     element='TMAX', start=1915, end=2017),
 Inventory(station='USC00010583', latitude=30.8839, longitude=-87.7853, 
     element='TMIN', start=1915, end=2017),
 Inventory(station='USC00012758', latitude=31.445, longitude=-86.9533, 
     element='TMAX', start=1890, end=2017)]

查看一下新列表中的前5条记录,看来情况不错。现在只包含温度记录了,开始和结束年份则显示出较久的数据持续时间。

剩下的问题就是选择最近的观测站了。为此,请将观测站inventory的经纬度与当前所在位置inventory的经纬度进行比较。获得某地经纬度的方法有很多,不过最简单的方法可能就是利用在线地图应用程序或在线搜索,纬度为41.882,经度为-87.629。

因为只对离当前所在位置最近的观测站感兴趣,所以这意味着需要根据站点的经纬度与当前位置的经纬度之间的距离进行排序。对列表进行排序很容易,按纬度和经度排序也不算太难。但如何按照经纬度的距离进行排序呢?

答案就是为排序定义一个键函数,该函数可以得到当前位置与观测站之间的纬度差和经度差,并将它们合并成一个数字。唯一要记住的是,需要在合并之前对差值取绝对值,以避免得到一个很大的负值和同样大的正值,这会给排序程序造成困惑:

# 通过在线地图获得市区经纬度
latitude, longitude = 41.882, -87.629

inventory_temps.sort(key=lambda x:  abs(latitude-x.latitude) + abs(longitude-
    x.longitude)) 

inventory_temps[:20]
Out[24]:
[Inventory(station='USC00110338', latitude=41.7806, longitude=-88.3092, 
     element='TMAX', start=1893, end=2017),
 Inventory(station='USC00110338', latitude=41.7806, longitude=-88.3092, 
     element='TMIN', start=1893, end=2017),
 Inventory(station='USC00112736', latitude=42.0628, longitude=-88.2861, 
     element='TMAX', start=1897, end=2017),
 Inventory(station='USC00112736', latitude=42.0628, longitude=-88.2861, 
     element='TMIN', start=1897, end=2017),
 Inventory(station='USC00476922', latitude=42.7022, longitude=-87.7861, 
     element='TMAX', start=1896, end=2017),
 Inventory(station='USC00476922', latitude=42.7022, longitude=-87.7861, 
     element='TMIN', start=1896, end=2017),
 Inventory(station='USC00124837', latitude=41.6117, longitude=-86.7297, 
     element='TMAX', start=1897, end=2017),
 Inventory(station='USC00124837', latitude=41.6117, longitude=-86.7297, 
     element='TMIN', start=1897, end=2017),
 Inventory(station='USC00119021', latitude=40.7928, longitude=-87.7556, 
     element='TMAX', start=1893, end=2017),
 Inventory(station='USC00119021', latitude=40.7928, longitude=-87.7556, 
     element='TMIN', start=1894, end=2017),
 Inventory(station='USC00115825', latitude=41.3708, longitude=-88.4336, 
     element='TMAX', start=1912, end=2017),
 Inventory(station='USC00115825', latitude=41.3708, longitude=-88.4336, 
     element='TMIN', start=1912, end=2017),
 Inventory(station='USC00115326', latitude=42.2636, longitude=-88.6078, 
     element='TMAX', start=1893, end=2017),
 Inventory(station='USC00115326', latitude=42.2636, longitude=-88.6078, 
     element='TMIN', start=1893, end=2017),
 Inventory(station='USC00200710', latitude=42.1244, longitude=-86.4267, 
     element='TMAX', start=1893, end=2017),
 Inventory(station='USC00200710', latitude=42.1244, longitude=-86.4267, 
     element='TMIN', start=1893, end=2017),
 Inventory(station='USC00114198', latitude=40.4664, longitude=-87.685, 
     element='TMAX', start=1902, end=2017),
 Inventory(station='USC00114198', latitude=40.4664, longitude=-87.685, 
     element='TMIN', start=1902, end=2017),
 Inventory(station='USW00014848', latitude=41.7072, longitude=-86.3164, 
     element='TMAX', start=1893, end=2017),
 Inventory(station='USW00014848', latitude=41.7072, longitude=-86.3164, 
     element='TMIN', start=1893, end=2017)]

4. 选择观测站并获取其元数据

在经过排序的新列表前20项数据中,似乎第一个站USC00110338就挺合适。它的TMIN和TMAX数据都齐全,持续时间也较长,从1893年开始一直持续到2017年,有超过120年的有效数据。因此,将该观测站存入站点变量中,并快速解析已抓取到的站点数据,以求多获取一点该站点的信息。

再回到readme文件,可以找到有关观测站数据的以下信息:

IV. FORMAT OF "ghcnd-stations.txt"

------------------------------
Variable   Columns   Type
------------------------------
ID            1-11   Character
LATITUDE     13-20   Real
LONGITUDE    22-30   Real
ELEVATION    32-37   Real
STATE        39-40   Character
NAME         42-71   Character
GSN FLAG     73-75   Character
HCN/CRN FLAG 77-79   Character
WMO ID       81-85   Character
------------------------------

These variables have the following definitions:

ID         is the station identification code.  Note that the first two
           characters denote the FIPS  country/area code, the third character 
           is a network code that identifies the station numbering system 
           used, and the remaining eight characters contain the actual 
           station ID. 

           See "ghcnd-countries.txt" for a complete list of country/area codes.
           See "ghcnd-states.txt" for a list of state/province/territory codes.

           The network code  has the following five values:

           0 = unspecified (station identified by up to eight 
           alphanumeric characters)
           1 = Community Collaborative Rain, Hail,and Snow (CoCoRaHS)
               based identification number.  To ensure consistency with
               with GHCN Daily, all numbers in the original CoCoRaHS IDs
               have been left-filled to make them all four digits long. 
               In addition, the characters "-" and "_" have been removed 
               to ensure that the IDs do not exceed 11 characters when 
               preceded by "US1". For example, the CoCoRaHS ID 
               "AZ-MR-156" becomes "US1AZMR0156" in GHCN-Daily
           C = U.S. Cooperative Network identification number (last six 
               characters of the GHCN-Daily ID)
           E = Identification number used in the ECA&D non-blended
               dataset
           M = World Meteorological Organization ID (last five
               characters of the GHCN-Daily ID)
           N = Identification number used in data supplied by a 
               National Meteorological or Hydrological Center
           R = U.S. Interagency Remote Automatic Weather Station (RAWS)
               identifier
           S = U.S. Natural Resources Conservation Service SNOwpack
               TELemtry (SNOTEL) station identifier
               W = WBAN identification number (last five characters of the 
               GHCN-Daily ID)

LATITUDE     is latitude of the station (in decimal degrees).

LONGITUDE    is the longitude of the station (in decimal degrees).

ELEVATION    is the elevation of the station (in meters, missing = -999.9).

STATE        is the U.S. postal code for the state (for U.S. stations only).

NAME         is the name of the station.
GSN FLAG     is a flag that indicates whether the station is part of the GCOS
             Surface Network (GSN). The flag is assigned by cross-referencing 
             the number in the WMOID field with the official list of GSN 
             stations. There are two possible values:

             Blank = non-GSN station or WMO Station number not available
             GSN   = GSN station 

HCN/        is a flag that indicates whether the station is part of the U.S.
CRN FLAG    Historical Climatology Network (HCN). There are three possible values:

             Blank = Not a member of the U.S. Historical Climatology 
                 or U.S. Climate Reference Networks
             HCN   = U.S. Historical Climatology Network station
             CRN   = U.S. Climate Reference Network or U.S. Regional Climate 
                 Network Station

WMO ID       is the World Meteorological Organization (WMO) number for the
             station.  If the station has no WMO number (or one has not yet 
             been matched to this station), then the field is blank.

尽管为了能更认真地进行研究,可能更应该关心元数据字段。但现在要做的,是把inventory记录的起始和结束年份与station文件中的观测站元数据对应起来。

station文件的筛选可以有好几种方案,只要能找到与所选观测站ID匹配的站点即可。可以创建一个for循环对每一行进行遍历,在找到之后跳出循环,也可以把数据按行拆分并排序,然后用二分法进行查找,凡此种种。根据已有数据的特点和数量,总有一种方法是合适的。

本例中的数据已经载入且数量并不太大,所以可用列表推导式返回一个列表,每个列表元素就是要做检索的观测站数据:

station_id = 'USC00110338'

# 解析站点
Station = namedtuple("Station", ['station_id', 'latitude', 'longitude', 
     'elevation', 'state', 'name', 'start', 'end'])

stations = [(x[0:11], float(x[12:20]), float(x[21:30]), float(x[31:37]),
     x[38:40].strip(), x[41:71].strip())
            for x in stations_txt.split("\n") if x.startswith(station_id)]

station = Station(*stations[0] + (inventory_temps[0].start,
     inventory_temps[0].end))
print(station)
Station(station_id='USC00110338', latitude=41.7806, longitude=-88.3092,
     elevation=201.2, state='IL', name='AURORA', start=1893, end=2017)

到目前为止,已经可以确定要观测站获取天气数据,并且拥有超过一个世纪的温度数据。

5. 获取并解析真实的天气数据

观测站确定之后,下一步就是获取该站的实际天气数据并进行解析。

1)获取数据

首先,要获取数据文件并保存下来,以备以后需要再次用到:

# 抓取已选取观测站的每日温度记录

r = requests.get('http://www.epubit.com:8083/quickpythonbook/daily/all/ 
     {}.dly'.format(station.station_id))
weather = r.text

# 保存为文本文件,这样就不需要再去抓取了

with open('weather_{}.txt'.format(station), "w") as weather_file:
    weather_file.write(weather)

# 按需从保存下来的每日数据文件中读取数据(只有在不想下载文件就开始处理过程时才会用到)

with open('weather_{}.txt'.format(station)) as weather_file:
    weather = weather_file.read()

print(weather[:540])
USC00110338189301TMAX  -11  6  -44  6 -139  6  -83  6 -100  6  -83  6  -72  6
    -83  6  -33  6 -178  6 -150  6 -128  6 -172  6 -200  6 -189  6 -150  6 -
    106  6  -61  6  -94  6  -33  6  -33  6  -33  6  -33  6    6  6  -33  6  
    -78  6  -33  6   44  6  -89 I6  -22  6    6  6
USC00110338189301TMIN  -50  6 -139  6 -250  6 -144  6 -178  6 -228  6 -144  6
    -222  6 -178  6 -250  6 -200  6 -206  6 -267  6 -272  6 -294  6 -294  6 
    -311  6 -200  6 -233  6 -178  6 -156  6  -89  6 -200  6 -194  6 -194  6 
    -178  6 -200  6  -33 I6 -156  6 -139  6 -167  6

2)解析天气数据

现在数据已经有了,还是可以看出这比station和inventory文件的数据要稍微复杂一些。显然,现在该回头看看readme.txt文件,第III节里有对天气数据文件的说明。

会有很多的条目可供选择,因此需要过滤一下,只看有关的条目,略过其他的元素类型,也略过整个指定了数据来源、品质和数值类型的标志体系:

III. FORMAT OF DATA FILES (".dly" FILES)

Each ".dly" file contains data for one station.  The name of the file corresponds to a station's identification code.  For example, 
     "USC00026481.dly" 
contains the data for the station with the identification code USC00026481).
Each record in a file contains one month of daily data.  The variables on each
line include the following:

-----------------------------
Variable   Columns   Type
------------------------------
ID            1-11   Character
YEAR         12-15   Integer
MONTH        16-17   Integer
ELEMENT      18-21   Character 
VALUE1       22-26   Integer
MFLAG1       27-27   Character
QFLAG1       28-28   Character
SFLAG1       29-29   Character
VALUE2       30-34   Integer
MFLAG2       35-35   Character
QFLAG2       36-36   Character
SFLAG2       37-37   Character
  .           .          .
  .           .          .
  .           .          .
VALUE31    262-266   Integer
MFLAG31    267-267   Character
QFLAG31    268-268   Character
SFLAG31    269-269   Character
------------------------------

These variables have the following definitions:

ID         is the station identification code. Please see "ghcnd-stations.txt"
           for a complete list of stations and their metadata.
YEAR       is the year of the record.

MONTH      is the month of the record.

ELEMENT    is the element type.   There are five core elements as well as a
           number of addition elements.  

       The five core elements are:

           PRCP = Precipitation (tenths of mm)
           SNOW = Snowfall (mm)
           SNWD = Snow depth (mm)
           TMAX = Maximum temperature (tenths of degrees C)
           TMIN = Minimum temperature (tenths of degrees C) 
...

VALUE1     is the value on the first day of the month (missing = -9999).

MFLAG1     is the measurement flag for the first day of the month.

QFLAG1     is the quality flag for the first day of the month.

SFLAG1     is the source flag for the first day of the month.

VALUE2     is the value on the second day of the month

MFLAG2     is the measurement flag for the second day of the month.

QFLAG2     is the quality flag for the second day of the month.

SFLAG2     is the source flag for the second day of the month.

... and so on through the 31st day of the month.  Note: If the month has less than31 days, then the remaining variables are set to missing (e.g., for April, VALUE31= -9999, MFLAG31 = blank, QFLAG31 = blank, SFLAG31 = blank).

现在应该关注的重点是,在一行数据中,观测站ID是前11个字符,年份是后面4个字符,月份是再后面两个字符,元素是再后面4个字符。然后,每日数据有31个槽位(slot),每个槽位包含5个字符的温度值,以十分之一摄氏度表示,以及3个字符的标志位。

如前所述,本次练习可以忽略标志位。大家还会发现,温度值如有缺失则用-9999编码表示。对于当月没有的日期,例如,典型的2月份,第29、30和31个温度值将会是-9999。

因为本次练习的数据处理,要得到的是整体趋势,所以不必太在意单日的数据,而要找到每月的平均值。可以把每月的最大值、最小值和平均值保存下来,以供使用。

这意味着要处理每一行天气数据,就需要:

  • 将每行拆分成各个独立的字段,并忽略或丢弃每个日数据的标志位;
  • 删除带-9999 的值,并将年份和月份转换为整数,将温度值转换为浮点数,同时请别忘了温度读数的单位为十分之一摄氏度;
  • 计算平均值,并挑出最高值和最低值。

为了完成上述全部任务,可以采取几种方案。可以对数据进行多次遍历,拆分为字段,去掉占位符,将字符串转换为数字,最后计算合计数。或者可以编写一个函数,对每一行数据执行所有操作,只用一次遍历就搞定。这两种方法都是有效的。这里采用后一种方法,创建一个parse_line()函数,用于执行所有数据转换操作:

def parse_line(line):
    """ parses line of weather data
        removes values of -9999 (missing value)
    """

    # 如果行为空则返回None
    if not line:
        return None
    # 拆分出前4个字段,以及包含温度值的字符串
    record, temperature_string = (line[:11], int(line[11:15]), 
     int(line[15:17]), line[17:21]), line[21:] 

    # 如果temperature_string长度不足,则引发异常
    if len(temperature_string) < 248:
        raise ValueError("String not long enough - {} 
     {}".format(temperature_string, str(line)))

    # 对temperature_string应用列表推导式,提取并转换温度数据
    values = [float(temperature_string[i:i + 5])/10 for i in range(0, 248, 8)
              if not temperature_string[i:i + 5].startswith("-9999")]

    # 获取温度数据的数量、最大值和最小值,计算平均值
    count = len(values)
    tmax = round(max(values), 1)
    tmin = round(min(values), 1)
    mean = round(sum(values)/count, 1)

    # 把温度的统计数据并入之前提取出4个字段的record中,并返回
    return record + (tmax, tmin, mean, count)

如果用第一行原始天气数据对该函数进行测试,将会得到以下结果:

parse_line(weather[:270]) 
Out[115]:
('USC00110338', 1893, 1, 'TMAX', 4.4, -20.0, -7.8, 31)

由此已有了一个可以解析数据的函数。如果该函数工作正常,下面就可以解析天气数据,然后保存下来或者继续处理:

# 处理所有的天气数据

# 列表推导,空行不会做解析
weather_data = [parse_line(x) for x in weather.split("\n") if x]

len(weather_data)

weather_data[:10]

[('USC00110338', 1893, 1, 'TMAX', 4.4, -20.0, -7.8, 31),
 ('USC00110338', 1893, 1, 'TMIN', -3.3, -31.1, -19.2, 31),
 ('USC00110338', 1893, 1, 'PRCP', 8.9, 0.0, 1.1, 31),
 ('USC00110338', 1893, 1, 'SNOW', 10.2, 0.0, 1.0, 31),
 ('USC00110338', 1893, 1, 'WT16', 0.1, 0.1, 0.1, 2),
 ('USC00110338', 1893, 1, 'WT18', 0.1, 0.1, 0.1, 11),
 ('USC00110338', 1893, 2, 'TMAX', 5.6, -17.2, -0.9, 27),
 ('USC00110338', 1893, 2, 'TMIN', 0.6, -26.1, -11.7, 27),
 ('USC00110338', 1893, 2, 'PRCP', 15.0, 0.0, 2.0, 28),
 ('USC00110338', 1893, 2, 'SNOW', 12.7, 0.0, 0.6, 28)]

现在已经有了全部的天气记录,都经过解析并保存在列表中,而不仅只有温度记录。

6. 将天气数据存入数据库

此时可以把所有天气记录保存到数据库中,必要的话再加上station和inventory记录。这样在以后的会话中,就能回来使用相同的数据,免去再次获取和解析数据的麻烦。

举例来说,以下就是将天气数据存入sqlite3数据库的代码:

import sqlite3

conn = sqlite3.connect("weather_data.db")
cursor = conn.cursor()

# 创建weather表

create_weather = """CREATE TABLE "weather" (
    "id" text NOT NULL,
    "year" integer NOT NULL,
    "month" integer NOT NULL,
    "element" text NOT NULL,
    "max" real,
    "min" real,
    "mean" real,
    "count" integer)"""
cursor.execute(create_weather)
conn.commit()

# 把经过解析的天气数据保存到数据库中

for record in weather_data:
    cursor.execute("""insert into weather (id, year, month, element, max, 
     min, mean, count) values (?,?,?,?,?,?,?,?) """, 
                      record)

conn.commit()

数据保存完毕后,就可以用类似以下代码从数据库中检索数据了,这里只读取TMAX记录:

cursor.execute("""select * from weather where element='TMAX' order by year,
      month""")
tmax_data = cursor.fetchall() 
tmax_data[:5]

[('USC00110338', 1893, 1, 'TMAX', 4.4, -20.0, -7.8, 31),
 ('USC00110338', 1893, 2, 'TMAX', 5.6, -17.2, -0.9, 27),
 ('USC00110338', 1893, 3, 'TMAX', 20.6, -7.2, 5.6, 30),
 ('USC00110338', 1893, 4, 'TMAX', 28.9, 3.3, 13.5, 30),
 ('USC00110338', 1893, 5, 'TMAX', 30.6, 7.2, 19.2, 31)]

7. 选取数据并作图

因为只关心温度,所以只需要选取温度记录。可以通过几个列表推导式来快速完成选取操作,得到一个TMAX列表和一个TMIN列表。或者可以利用pandas的功能,用于对每日数据作图,并滤除不需要的记录。

因为更关注纯Python而不是pandas,所以这里采取第一种方案:

tmax_data = [x for x in weather_data if x[3] == 'TMAX'] 
tmin_data = [x for x in weather_data if x[3] == 'TMIN'] 
tmin_data[:5]

[('USC00110338', 1893, 1, 'TMIN', -3.3, -31.1, -19.2, 31),
 ('USC00110338', 1893, 2, 'TMIN', 0.6, -26.1, -11.7, 27),
 ('USC00110338', 1893, 3, 'TMIN', 3.3, -13.3, -4.6, 31),
 ('USC00110338', 1893, 4, 'TMIN', 12.2, -5.6, 2.2, 30),
 ('USC00110338', 1893, 5, 'TMIN', 14.4, -0.6, 5.7, 31)]

8. 用pandas对数据绘图

此时数据已清洗完毕,绘制图表的准备工作已经完成。为了简化绘制工作,可以利用pandas和matplotlib。为此需要运行Jupyter服务器并安装pandas和matplotlib。

为了在Jupyter记事本中确保它们已经安装完毕,请使用以下命令:

# 用pip安装pandas和matplotlib
! pip3.6 install pandas matplotlib

import pandas as pd 
%matplotlib inline

pandas和matplotlib安装完毕后,就可以加载pandas并由TMAX和TMIN数据创建Data Frame了。

tmax_df = pd.DataFrame(tmax_data, columns=['Station', 'Year', 'Month', 
     'Element', 'Max', 'Min', 'Mean', 'Days'])
tmin_df = pd.DataFrame(tmin_data, columns=['Station', 'Year', 'Month',
     'Element', 'Max', 'Min', 'Mean', 'Days'])

当然可以绘制月度数据,但123年乘以12个月的数据差不多有1500个数据点,而季节的交替也会给数据选取模板的确定带来难度。

将每月的最高温度、最低温度和平均数值按年度平均,再绘制成图标,反而可能更有意义。

用Python当然可以完成这些操作,但因为数据已载入了pandas的Data Frame中,所以可以用Data Frame按年分组并获取平均值:

# 选取年份、最低、最高、平均数这几列,按年分组并计算平均值,绘制折线图

tmin_df[['Year','Min', 'Mean', 'Max']].groupby('Year').mean().plot(
     kind='line', figsize=(16, 4))

上述语句的返回结果会有很多变数(依据不同的站点和时间段),但似乎表明过去20年来的最低温度一直在上升。

注意,如果不想用Jupyter记事本和matplotlib获得同样的图表,pandas还是可以用的,但是应该用Data Frame的方法to_csv()或to_excel()写入CSV或Microsoft Excel文件。然后可以把生成的文件加载到电子表格中,并在其中生成图表。

标签:Python,NaN,element,station,绘图,txt,数据,pandas
From: https://blog.51cto.com/u_11837698/6215470

相关文章

  • python| 关于excel的文件处理
    创建一个成绩单文件score.xlsx,将平时成绩单.xlsx文件中对应班级工作表中学号和姓名列的内容写入到score.xlsx中,并添加成绩列,每个学生的成绩采用随机生成的一个分数填写进去,最后统计所有学生的平均成绩计算出来后,写入到score.xlsx的最后一行最后一列之后的单元格中去。预想的步骤:1......
  • 常见算法Python实现
    一、算法与数据结构1、二叉树1.重建二叉树输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如,输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建如下图所示的二叉树并输出它的头节......
  • Python 设计模式详解
    一、创建型模式1、工厂方法 Factory工厂方法是一种创建型设计模式,其在父类中提供一个创建对象的方法,允许子类决定实例化对象的类型制造业是一个国家工业经济发展的重要支柱,而工厂则是其根基所在。程序设计中的工厂类往往是对对象构造、实例化、初始化过程的封装,而工厂方法则可以升......
  • 旋转图像--Python实现
    给定一个n×n的二维矩阵matrix表示一个图像。请将图像顺时针旋转90度。defrotate(matrix):"""Donotreturnanything,modifymatrixin-placeinstead."""matrix[:]=zip(*matrix[::-1])returnmatrix......
  • Python习题
    文本词频统计题目:一篇文章,出现了哪些词?哪些词出现的最多?请统计hamlet.txt文件中出现的英文单词情况,统计并输出出现最多的10个单词,注意:(1)单词不区分大小写,即单词的大小写或组合形式一样;‪‬‪‬‪‬‪‬‪‬‮‬‫‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‭‬‪‬‪‬‪‬‪‬‪‬‪‬‮......
  • 【m3u8】python使用m3u8库下载视频
    1、m3u8库https://pypi.org/project/m3u8/ 2、安装pipinstallm3u8  3、使用importtimefromCrypto.Util.PaddingimportpadfromCrypto.CipherimportAESimportrequestsimportm3u8headers={"User-Agent":"Mozilla/5.0(WindowsNT10.......
  • 【Python】尝试切换py版本
    失败问chatgpt,怎么把abaquspython版本切换到py3.6,结果失败。chatgpt给出的建议:修改abaqus_v6.env,明显扯淡!我就尝试在custom_v6.env中添加python路径,结果就是开头的报错。其他有用的回答:怎么查看abaqus2020当前使用的Python的版本信息importsysprint(sys.version)......
  • Python基础—conda使用笔记
    Python基础—conda使用笔记1.环境配置由于用conda管理虚拟环境真滴很方便,所以主要使用conda,就不单独去装Python了。1.1.Miniconda3安装Miniconda3官网下载地址:MinicondaMiniconda3清华镜像下载:清华镜像-Miniconda对于Windows系统:Miniconda安装跟正常的软件安装是一样......
  • python学习-学生信息管理系统并打包exe
    在B站自学Python站主:Python_子木授课:杨淑娟平台:马士兵教育python:3.9.9python打包exe文件#安装PyInstallerpipinstallPyInstaller#-F打包exe文件,stusystem\stusystem.py到py的路径,可以是绝对路径,可以是相对路径pyinstaller-Fstusystem\stusystem.py学生信息管理......
  • Python | setattr函数的使用
    在Python中,setattr()是一个内置函数,用于设置对象的属性值,该属性不一定是存在的。语法setattr()的语法如下:setattr(obj,name,value)其中,obj是要设置属性值的对象,name是要设置的属性名,value是要设置的属性值。返回值为无。示例用法示例一:classPerson:def__in......