想入门Pandas,那么首先需要了解Pandas中的数据结构。因为Pandas中数据操作依赖于数据结构对象。Pandas中最常用的数据结构是:SeriesDataFrame。这里可以将Series和DataFrame分别看作一维数组和二维数组。

数据结构

Series

Series是一维标签数组,其可以存储任何数据类型,包括整数,浮点数,字符串等等。所谓标签数组,这里的标签即是指Series的索引。

1
import pandas as pd
1
s=pd.Series([5,4,3,2,1], index=['a', 'c', 'e', 3, 1])

⚠️ 创建时给定了一个列表: [5,4,3,2,1],并且通过 index 参数用于指定索引。如果仅给定列表,不指定index参数,默认索引为从0开始的数字。注意:索引标签为字符串和整数的混合类型。记住不要使用浮点数作为索引,并且尽量避免使用混合类型索引

除了使用传入列表或者numpy数组之外,也可以通过字典的方式创建:

1
s=pd.Series({'a':5, 'b':4, 'c':3, 'd':2, 'e':1})

DataFrame

DataFrame是一种表格型数据结构,可以看作是具有行列标签的二维数组。每列可以是不同类型的数据,比如数值,字符串,逻辑值等。

DataFrame的创建有多种方式,比较常用的是通过字典的方式创建,此外,还可以给定数组,通过指定columns和index参数创建:

1
2
d1=pd.DataFrame({'one':[1,3,5], 'two':[2,4,6]})  # 不指定索引,默认仍从0开始。
d2=pd.DataFrame([[1,2],[3,4],[5,6]], columns=['one', 'two'], index=['a', 'b', 'c'])

简单的介绍了Series和DataFrame这两种数据结构之后,我们以全国空气质量历史数据(http://beijingair.sinaapp.com)为例,通过实际的数据处理来介绍一下常用的操作。

数据为逗号分隔的csv格式数据,数据存储如下:

数据存储形式

数据存储以**逗号*作为分隔符,列为: date, hour, type, 1001A, 1002A…,date和hour为时间信息列,type为对应的要素,其余的列均为站点名称。

数据处理

1
data = pd.read_csv('china_sites_20170101.csv', sep=',')

由于文件中存储了多行多列数据,因此,完全读取之后 data 为 DataFrame 类型。

探索性分析

  • 查看DataFrame数据信息:
1
2
3
4
data.shape 
data.ndim  # 获取数据的维度信息
data.index  # 获取索引
data.columns #获取列名

  • 查看数据行列对象信息
1
data.info() 
1
2
3
4
5
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 285 entries, 0 to 284
Columns: 1500 entries, date to 2846A
dtypes: float64(1497), int64(2), object(1)
memory usage: 3.3+ MB

上述数据中包含285行,1500列,其中type列为object,date和hour列为int64类型,其余列均为float64类型。memory表明数据总共占用了约3.3M内存。

  • 数据统计信息

获取每一列的统计相关数据,count表示一列的行数,mean表示均值,std为标准差,minmax表示最小值和最大值,25%50%75%分别表示1/4位数,中位数和3/4位数。

1
data.describe()

⚠️ describte 仅统计数值型列的统计数据,对于object列,会直接忽略。

这里还要注意一点:由于type列对应了不同的空气质量要素,而不同的空气质量要素具有不同的取值范围,因此在使用describe查看统计信息时,应针对不同的要素进行,这样才有具体意义,才能看出每个要素的值分布,以及确定是否存在异常值。

  • 简单的数据查看

head 方法可以查看整个数据集的前几行信息,默认是前5行,但可以指定参数选择,与 head 对应的是 tail 可以查看对应的从末尾开始的默认5行数据。这两个方法类似linux中的 head 和 tail 命令。

1
2
data.head()
data.tail()
  • 数据选择

简单的了解了上述信息之后,我们对不同的空气质量要素进行操作时就要涉及到数据的选择。

⚠️ Pandas官方提示:以下切片形式操作在简单的交互式数据分析时是非常友好的,但是如果应用于生产环境尽量使用优化后的一些方法:.at.iat.loc.iloc.ix等。

Pandas主要有两种数据查询选择操作:

  • 基于标签的查询
  • 基于整数的位置索引查询

Pandas在选择列时,无需使用date[:, columns]的形式,先使用 : 选择所有行,再指定 columns 列名称。选择1001A站点的数据。

1
2
data[['date', 'hour', 'type', '1001A']]  # 获取四列所有行数据,仍为DataFrame
data[0:5] # 选择所有列前5行数据,仅包括索引0-4行

超纲题:由于数据中包含了时间信息列(date和hour),为了方便操作,我们可以将时间列设置为索引。

1
2
3
   date_index = pd.to_datetime(data.date.apply(lambda x: str(x)) + data.hour.apply(lambda x: '%02d'%x), format='%Y%m%d%H')
   data.index = date_index
   # data.drop(['date', 'hour], axis=1, inplace=True)  ## 删除 date和hour列,inplace选项直接针对原DataFrame操作

https://ws3.sinaimg.cn/large/006tNbRwgy1fxr5gk54fxj31eu0fi412.jpg

  • 基于标签的查询 .loc

.loc 主要基于标签进行数据选择,此外还可以使用逻辑数组。当所选择的项不存在时会诱发异常。

  • 单个标签

    1
    
    data.loc[:, '1001A'] # 返回Series 注意 : 行索引,如果仅给定 data.loc['1001A'] 会出错
  • 标签数组

    1
    
    data.loc[:, ['1001A', '1006A', '2706A']]
  • 标签切片对象

    1
    2
    
    data.loc['2017-01-01 00:00:00':'2017-01-01 06:00:00', '1001A':'1005A']  # 针对行和列均进行切片
    # data.loc[0:5, '1001A':'1005A] # 会出错

    ⚠️ 由于行索引已经转换为时间,因此此处不能使用 整数 索引。因为 .loc 只能用于行列标签索引,整数位置索引需要使用 .iloc

  • 逻辑数组

    1
    
    data.loc[data['type'] == 'AQI']  # 选择所有站点的AQI数据
  • 可调用函数

    即可以传入函数作给 .loc ,但函数返回结果应是有效的索引,比如标签或者逻辑数组

    1
    2
    3
    4
    
    def test(data, column, name):
        return data[columns] = name
        
    data.loc[test(data, 'type', 'AQI')]
  • 基于整数的位置索引查询 .iloc

.iloc 主要是基于整数的位置索引,也可以使用逻辑数组的方式。如果索引越界会诱发IndexError错误,但切片索引允许索引越界

  • 单个整数

    1
    
    data.iloc[0]  # 返回第1行的所有列,结果为Series
  • 整数数组

    1
    
    data.iloc[[0,2,4,6,8], [0,1,2,3]]
  • 整数切片

    1
    
    data.iloc[0:10, 0:4]
  • 逻辑数组

    1
    
    data.iloc[(data.type == 'AQI').values]  # 获取所有站点的AQI数据 

    ⚠️ 由于 data.type == ‘AQI’ 返回的是 Series,我们只需要获取其中的值,因此指定 .values 属性。

  • 可调用函数

    • 传入可调用函数给 .iloc,函数返回值应为:单个整数,整数数组,数组切片或者逻辑数组。

    上述两种数据选择虽是基于DataFrame,但Series也支持同样的操作,以1001A 站点的AQI数据为例:

    1
    
    s = data.loc[data.type == 'AQI']['1001A']

    由于Series只有一列,因此只需要对行进行索引操作即可,也支持基于标签和整数的位置索引方式。

    1
    2
    
    s.loc['2017-01-01 06:00:00':'2017-01-01 12:00:00']
    s.iloc[2:10]
  • 重建索引

通过观察1001A站点的Series数据可以发现:某些时刻的数据缺失。对于时间序列数据而言,数据的缺失可能会导致分析时出现问题。因为,我们需要补齐所有时刻。

1
2
3
from datetime import datetime

date_new = pd.date_range(datetime(2017, 1, 1, 0), datetime(2017, 1, 1, 23), freq='1h')
1
data.reindex(date_new)  # 重新索引
  • 缺失值

补齐所有时刻之后,我们可以查看一下数据的缺失情况:

1
2
data.isnull() # 返回逻辑DataFrame,缺失值为True,否则为False
# data.isnull().sum()  # 统计每个站点每个要素的总的缺失数

data.isnull().sum() 利用了逻辑运算时:True被视为1,False被视为0的方式。

data.isnull() 相反的是 data.notnull() ,是缺失值为False,否则为True。

如果想丢弃缺失值,可使用 .dropna 方法,即 data.dropna()

但对于时间序列而言,一般不选择直接丢弃缺失时刻,否则可能造成时间缺失,破坏连续性。因此,可以选择补齐数据。

1
2
data.fillna() # fillna 使用给定值和方法进行数据填补
   data.interpolate() # interpolate 可以通过线性插值等方法通过插值补齐数据

  • 统计计算

Pandas中Series和DataFrame均包含一些常用的统计计算方法,比如:

1
2
3
4
data.mean()  # 计算平均值
data.sum()   # 求和
data.std()   # 计算标准差
data.median() # 获取中位数

上述数据是2017年1月1日全国所有观测站观测的常规要素逐小时数据,上面几个统计命令均是对每个站点每个要素进行计算。此外,也可以对单个站点分时刻计算,比如:

1
data3['1001A'].resample('6h').mean() # 针对1001A站点,进行每6小时求平均

.resample 是重采样方法,其返回一个对象,然后对此对象执行 .mean 求均值方法。 .mean 也可以换为 .sum.std 等方法。

对于时间跨度比较长的数据,也可以求逐日平均,逐月平均等等。DataFrame.resmaple('1d').mean() , DataFrame.resample('2m').mean()

  1. * 对行或列应用函数: .apply

上面在创建时间索引时便利用了.apply 方法,对date 和 hour列分别进行了数据类型的转换,然后将两个字符串进行了连接,转换为时间。

1
   pd.to_datetime(data.date.apply(lambda x: str(x)) + data.hour.apply(lambda x: '%02d'%x), format='%Y%m%d%H')
  1. * 字符串函数

Series中提供了大量的字符串函数,可以对字符串类型的数据进行常规操作。比如想替换字符串,或者转换字符串大小写等等。

1
2
   data.type.str.replace('PM2.5', 'PM2_5') # 或 data.type.replace('PM2.5', 'PM2_5')
   data.type.str.lower()

  • 轴转换

目前的数据存储形式是:站点作为列,每个站点的空气质量要素通过 type 列单独给定。有时候这种存储形式并不方便,我们想要为以下形式:

即获取每个站点时,可以直接获取当前站点的所有要素数据,而且时间索引也按照单个时刻排列,索引不会出现重复值,而之前的存储形式索引会出现重复。索引重复会使得某些操作出错。

我们可以通过 .pivot_table 进行转换:

1
data.pivot_table(index=data.index, columns=['type'])

索引仍为 data 的原索引,但对 type 列进行旋转。旋转完成之后返回的DataFrame的列为 MultiIndex。而关于 MultiIndex 的查询操作属于高级主题。

对于 MultiIndex 的操作,同样可以使用.loc 方法,并借助 .IndexSlice 进行索引。

 索引切片: 可以理解成 idx 将 MultiIndex 视为一个新的 DataFrame,然后将上层索引视为行,下层索引视为列,以此来进行数据的查询。

1
sub=data.loc[:, idx['1001A', ['AQI', 'PM10', 'PM2.5']]]

上述操作返回的列仍然是 MultiIndex,因为此时只有一个站点了,我们可以使用 .xs 方法将列从MultiIndex转换为Index

1
sub.xs('1001A', axis=1)

简单绘图

关于Python可视化工具我们提到过数据处理和可视化一条龙服务的Pandas,Pandas不仅可以进行数据处理工作,而且其还封装了一些绘图方法。首先导入 matplotlibseaborn,这是为了能够较好的显示图形比例。

1
2
3
4
import matplotlib.pyplot as plt
import seaborn as sns

sns.set_context('talk', font_scale=1.3)

这里我们选择单个站点数据进行画图:

1
2
3
4
sub = data.loc[:, idx['1001A', :]].xs('1001A', axis=1)

fig, ax = plt.subplots(figsize=(16, 9))
sub.plot.box(ax=ax)

上图可以看出:不同的要素其值所在范围是不同的,在探索性分析时应分开分析。

除了箱线图之外,Pandas还可以绘制折线图,条形图,饼图,密度分布等。这在数据分析时是比较方便的,但在图形美化或其他图形绘制还需要借助其他工具,比如统计绘图Seaborn更胜一筹。