学机器学习,不会数据处理怎么行?—— 二、Pandas详解

在上篇文章学机器学习,不会数据处理怎么行?—— 一、NumPy详解中,介绍了NumPy的一些基本内容,以及使用方法,在这篇文章中,将接着介绍另一模块——Pandas。(本文所用代码在这里

Pandas数据结构介绍

大家应该都听过表结构,但是,如果让你自己来实现这么一个结构,并且能对其进行数据处理,能实现吗?我相信,大部分人都能做出来,但是不一定能做的很好。而Python中的一个模块pandas给我们提供了一个很好的数据结构,它包括了序列Series和数据框DataFrame。pandas是基于NumPy数组构建的,特别是基于数组的函数和不使用for循环的数据处理,让以Numpy为中心的应用变得更加简单。

Series

创建方式

Series是一种类似于一维数组的对象,它由一组数据(各种NumPy数据类型)以及一组与之相关的数据标签(即索引)组成,其创建主要有三种方式

1)通过一维数组创建序列

import numpy as np
import pandas as pd
arr1 = np.arange(10)
s1 = pd.Series(arr1)

通过type函数可查看arr1与s1的类型分别为,numpy.ndarray 和 pandas.core.series.Series。

3)通过字典的方式创建

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

通过type函数可查看dic1与s2的类型分别为,dict 和 pandas.core.series.Series。

3)通过DataFrame中的某一行或者某一列创建序列

这一步将在下面讲DataFrame的时候再补充

Series索引

我们上面在创建Series序列时,可以输出查看我们创建的Series长什么样,发现有两列,第二列的数据跟我们输入的是一样的,那第一列是什么呢?那就是索引。Series的字符串表现形式为:索引在左边,值在右边。如果我们没有为数据指定索引,就会自动创建一个0到N-1(N为数据的长度)的整数型索引。

 我们可以通过 Series 的 values 和 index 属性获取其数组表示形式和索引对象。

obj = pd.Series([6, 9, -8, 1])
obj.values
obj.index

我们也能修改其索引

obj.index = ['a','b','c','d']

讲了那么多,知道这是索引了,但它有什么用呢? 

1.我们可以通过索引值或索引标签进行数据的获取

obj['a']
obj[3]
obj[[0,2,3]]
obj[0:2]
obj['a':'c']

注:若通过索引标签获取数据的话,末端标签对应的值也将返回!!!

2.自动化对齐

如果有两个序列,需要对这两个序列进行算术运算,这时索引的存在就体现的它的价值了—自动化对齐.

obj1 = pd.Series(np.array([10,15,20,30,55,80]),index = ['a','b','c','d','e','f'])
obj2 = pd.Series(np.array([12,11,13,15,14,16]),index = ['a','c','g','b','d','f'])
obj1+obj2
obj1*obj2

我们会发现,计算后的序列号中,g和e对应的值为NaN,这是因为obj1中没有g索引,obj2中没有e索引,所以数据的运算会产生两个缺失值NaN。

DataFrame

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

创建方式

1)通过二维数组创建

arr2 = np.array(np.arange(12).reshape(4,3))
df1 = pd.DataFrame(arr2)

 

2)通过字典的方式创建

该方式可通过字典列表和嵌套字典实现,如果将嵌套字典传给DataFrame,pandas就会被解释为:外层字典的键作为列,内层键则作为行索引

#字典列表
dic2 = {'a': [1, 2, 3, 4],'b': [5, 6, 7, 8],'c': [9, 10, 11, 12],'d': [13, 14, 15, 16]}
df2 = pd.DataFrame(dic2)

#嵌套字典
dic3 = {'one': {'a': 1,'b': 2,'c': 3,'d':4},
        'two': {'a': 5,'b': 6,'c': 7,'d': 8},
        'three':{'a': 9,'b': 10,'c': 11,'d':12}}
df3 = pd.DataFrame(dic3)

 

3)通过数据框的方式创建

df4 = df3[['one','three']]

s3 = df4['one']

 

DataFrame索引

DataFrame的索引与Series的索引大体上是一样的,不过DataFrame有两个索引,分别是列索引和行索引,感觉看起来就跟excel差不多了。具体的实现方式可通过下面的部分来了解。

利用pandas进行数据处理

pandas可以通过布尔索引有针对的选取原数据的子集、指定行、指定列等,同时我们可以通过 pd.read_csv()来导入数据集,因为暂时找不到数据集,就从别人的代码里复制一些过来了

data = {'state': ['Ohio', 'Ohio', 'Ohio', 'Nevada', 'Nevada', 'Nevada'],
        'year': [2000, 2001, 2002, 2001, 2002, 2003],
        'pop': [1.5, 1.7, 3.6, 2.4, 2.9, 3.2]}
frame = pd.DataFrame(data)

 

选择数据

简单筛选

我们可以使用以下函数来查询数据的前几行或后几行(默认5行)

frame.head()
frame.tail()

也可以使用行列索引来简单地查询指定的行和列

frame['state']
frame[0:3]

 

loc

当然,上面的方式只是比较简单的操作,复杂些的,我们可以通过 loc 来选取数据(: 表示所有行)

frame.loc[:,['state','year']]

 

iloc

另外,我们也可以使用iloc,通过位置选择在不同情况下所需要的数据,可进行选取某个数据、连续或者跨行选择等操作

frame.iloc[0:2,0:2]

 

ix

除此之外还有一种操作方式,使用 混合选择 ix ,我这里的表可能无法很好的体现,如果将行索引改为字符就可以看出来了

frame.ix[3:5,['year']]

发现输入这行代码运行之后发出一条警告

DeprecationWarning: 
.ix is deprecated. Please use
.loc for label based indexing or
.iloc for positional indexing

emmmmm。。想想以后主要还是用 loc 和 iloc 好了。

通过判断的筛选

除了上面讲的之外,我们还可以通过判断指令进行选择,即通过约束某项条件然后选择出当前所有数据。

frame[frame['year']>2001]

 

数据修改

讲完了数据选择,既然选择出了数据,我们可能会想去修改数据,那么,怎么修改呢?

根据位置

我们可以利用索引或者标签确定需要修改的位置

frame.iloc[0,2]=1.6
frame.loc[1,'pop']=2

 

根据条件

如果我们想将数据中’pop’栏小于1.7的’year’改为1999,那么我们可以进行如下操作

frame.year[frame['pop']<1.7]=1999

 

按行或按列设置

我们还可以对行列进行批处理

frame['pop']=2.6

 

添加和删除数据

上面讲的都是数据的修改,再来讲讲数据的添加和删除

#数据添加
#方法一
frame['A'] = np.nan
#方法二
frame['B'] = pd.Series([1, 2, 3, 4, 5, 6])

#数据删除 
frame.drop('B',axis = 1)

注:

  • 对于方法二,长度必须对齐,如果行索不为默认则需指定索引
dates = pd.date_range('20130101', periods=6)
df = pd.DataFrame(np.arange(24).reshape((6,4)),index=dates, columns=['A','B','C','D'])
df['E'] = pd.Series([1, 2, 3, 4, 5, 6],index=pd.date_range('20130101',periods=6))

 

  • 对于数据删除,axis取值 0表示删除行索引 1表示删除列索引 默认为0

统计分析

pandas模块为我们提供了非常多的描述性统计分析的指标函数,如总和、均值、最小值、最大值等,跟对应的NumPy数组方法相比,它们都是基于没有缺失数据的假设而构建的,下图中列举了大部分函数

这里有几点需要注意

  • descirbe函数只能针对序列或者数据框,一维数组是没有这个方法的。
  • 使用sum时,NaN值会自动被排除,除非整个切片都是NaN,通过skipna选项可以禁用该功能。
  • 调用sum时,返回的时含有列的和的Series,需要传入axis的值才会进行行运算

当我们要对一列数据进行分析时,我们可以将要分析的内容封装成一个函数,方便调用

def stats(x):
    return pd.Series([x.count(),x.min(),x.idxmin(),
    x.quantile(.25),x.median(),
    x.quantile(.75),x.mean(),
    x.max(),x.idxmax(),
    x.mad(),x.var(),
    x.std(),x.skew(),x.kurt()],
    index = ['Count','Min','Whicn_Min',
    'Q1','Median','Q3','Mean',
    'Max','Which_Max','Mad',
    'Var','Std','Skew','Kurt'])

有时或许会需要对一个数据框进行分析,那该如何操作?是一列一列不断地取出来然后调用函数吗?这个方法也不是不可行,但面对众多数据时该怎么办?一个一个调用怕不是要弄到明年,在这里我们可以使用apply函数

d1 = pd.Series(2*np.random.normal(size = 100)+3)
d2 = np.random.f(2,4,size = 100)
d3 = np.random.randint(1,100,size = 100)
df = pd.DataFrame(np.array([d1,d2,d3]).T,columns=['x1','x2','x3'])
df.head()
df.apply(stats)

就这样很简单的创建了数值型数据的统计性描述。但如果是离散型数据呢?就不能用这个统计口径了,我们需要统计离散变量的观测数、唯一值个数、众数水平及个数。你只需要使用describe方法就可以实现这样的统计了。

缺失值处理

现实生活中的数据是非常杂乱的,其中缺失值也是非常常见的,对于缺失值的存在可能会影响到后期的数据分析或挖掘工作,那么我们该如何处理这些缺失值呢?常用的有三大类方法,即删除法、填补法和插值法。

删除法:当数据中的某个变量大部分值都是缺失值,可以考虑删除改变量;当缺失值是随机分布的,且缺失的数量并不是很多是,也可以删除这些缺失的观测。
替补法:对于连续型变量,如果变量的分布近似或就是正态分布的话,可以用均值替代那些缺失值;如果变量是有偏的,可以使用中位数来代替那些缺失值;对于离散型变量,我们一般用众数去替换那些存在缺失的观测。
插补法:插补法是基于蒙特卡洛模拟法,结合线性模型、广义线性模型、决策树等方法计算出来的预测值替换缺失值。

删除法

这里仅讲述删除法和替补法,先来看删除法,先建立一个6X4的矩阵数据并且把两个位置置为空

dates = pd.date_range('20130101', periods=6)
df = pd.DataFrame(np.arange(24).reshape((6,4)),index=dates, columns=['A','B','C','D'])
df.iloc[0,1] = np.nan
df.iloc[1,2] = np.nan

我们可以结合sum函数和isnull函数来检测数据中含有多少缺失值

for i in df.columns:
    print(sum(pd.isnull(df[i])))

也可以通过any来判断是否存在缺失值

np.any(df.isnull()) == True  

删除直接dropna就行了

df.dropna(
    axis=0,     # 0: 对行进行操作; 1: 对列进行操作 默认为0
    how='any'   # 'any': 只要存在 NaN 就 drop 掉; 'all': 必须全部是 NaN 才 drop 默认为'any'
    ) 

填补法

简单粗暴的办法就是把所有的NaN用0代替

df.fillna(value=0)

稍微好一点的方法,使用常量填充不同的列

df.fillna({'B': 3,'C': 4})

还算好的方法,采用前项填充或后向填充

df.fillna(method = 'ffill') #用前一个观测值填充
df.fillna(method = 'bfill') #用后一个观测值填充

较好的方法,用均值或中位数填充各自的列

df.fillna(df.median())
df.fillna(df.mean())

很显然,在使用填充法时,相对于常数填充或前项、后项填充,使用各列的众数、均值或中位数填充要更加合理一点,这也是工作中常用的一个快捷手段。

注:使用fillna,dropna时,需要添加参数 inplace = True,如df.fillna(df.median(),inplace = True),以确认修改,否则实际的数据并不会有改动。

数据合并

concat

pandas 处理多组数据的时候往往会要用到数据的合并处理,使用 concat 是一种基本的合并方式.而且concat中有很多参数可以调整,合并成你想要的数据形式.

先创建数据集

df1 = pd.DataFrame(np.ones((3,4))*0, columns=['a','b','c','d'])
df2 = pd.DataFrame(np.ones((3,4))*1, columns=['a','b','c','d'])
df3 = pd.DataFrame(np.ones((3,4))*2, columns=['a','b','c','d'])

concat合并

pd.concat([df1, df2, df3], axis=0) #纵向合并
pd.concat([df1, df2, df3], axis=1) #横向合并

这里观察下纵向合并的输出,会发现index的值为0,1,2,0,1,2,0,1,2,大部分情况下,这肯定不是我们想要的,我们可以通过一下方法来重置index

pd.concat([df1, df2, df3], axis=0, ignore_index=True)

pandas中的数据结构是支持类似SQL的部分操作方式的,如添加新行新列、多表连接、聚合什么的。pandas有个join参数,用来定义连接方式的,默认为outer,此方式是依照column来做纵向合并有相同的column上下合并在一起,其他的独自成列,没有的值用NaN填充

df1 = pd.DataFrame(np.ones((3,4))*0, columns=['a','b','c','d'], index=[1,2,3])
df2 = pd.DataFrame(np.ones((3,4))*1, columns=['b','c','d','e'], index=[2,3,4])
res = pd.concat([df1, df2], axis=0, join='outer')#纵向"外"合并df1与df2

参数:

  • axis 表示行或列,0为行,1为列
  • join ‘inner’:内连接,会产生NaN的列丢弃 ‘outer’:外连接,上面介绍过了

另外,还有join_axes也可以决定连接方式

pd.concat([df1, df2], axis=1, join_axes=[df1.index])
pd.concat([df1, df2], axis=1, join_axes=[df2.index])

运行上面两行代码,就会发现有所不同,第一行是以df1的index为依据,将df2中具有相同索引的行对应拼接上去,第二行同样的道理。

此外,还有append操作,该操作类似与axis=0,join=’outer’时的操作,不过要注意的是append只有纵向合并。

merge

pandas中的合并操作除了concat之外,还有merge操作,主要用于两组有key column的数据,统一索引的数据. 通常也被用在Database的处理当中.

看个简单的,依据一组Key合并

left = pd.DataFrame({'key': ['K0', 'K1', 'K2', 'K3'],
                             'A': ['A0', 'A1', 'A2', 'A3'],
                             'B': ['B0', 'B1', 'B2', 'B3']})
right = pd.DataFrame({'key': ['K0', 'K1', 'K2', 'K3'],
                              'C': ['C0', 'C1', 'C2', 'C3'],
                              'D': ['D0', 'D1', 'D2', 'D3']})
pd.merge(left, right, on='key')

稍微难一点就是依据两组Key合并,合并时有四种方法 how=[‘left’,’right’,’outer’,’inner’],默认为how=’inner’

left = pd.DataFrame({'key1': ['K0', 'K0', 'K1', 'K2'],
                      'key2': ['K0', 'K1', 'K0', 'K1'],
                      'A': ['A0', 'A1', 'A2', 'A3'],
                      'B': ['B0', 'B1', 'B2', 'B3']})
right = pd.DataFrame({'key1': ['K0', 'K1', 'K1', 'K2'],
                       'key2': ['K0', 'K0', 'K0', 'K0'],
                       'C': ['C0', 'C1', 'C2', 'C3'],
                       'D': ['D0', 'D1', 'D2', 'D3']})

res = pd.merge(left, right, on=['key1', 'key2'], how='inner')
print(res)

res = pd.merge(left, right, on=['key1', 'key2'], how='outer')
print(res)

res = pd.merge(left, right, on=['key1', 'key2'], how='left')
print(res)

res = pd.merge(left, right, on=['key1', 'key2'], how='right')
print(res)

都试一试然后输出就知道是如何操作的了。

 上面讲的是以列索引为标准进行合并,当然,我们还可以以index为标准进行合并

left = pd.DataFrame({'A': ['A0', 'A1', 'A2'],
                     'B': ['B0', 'B1', 'B2']},
                     index=['K0', 'K1', 'K2'])
right = pd.DataFrame({'C': ['C0', 'C2', 'C3'],
                      'D': ['D0', 'D2', 'D3']},
                     index=['K0', 'K2', 'K3'])

res = pd.merge(left, right, left_index=True, right_index=True, how='outer')
print(res)

res = pd.merge(left, right, left_index=True, right_index=True, how='inner')
print(res)

 聚合和分组

pandas提供了一个灵活高效的groupby功能,它使你能以一种自然的方式对数据集进行切片、切块、摘要等操作。根据一个或多个键(可以是函数、数组或DataFrame列名)拆分pandas对象。计算分组摘要统计,如计数、平均值、标准差,或用户自定义函数。

根据列索引分组

df = pd.DataFrame({'key1':['a', 'a', 'b', 'b', 'a'],
        'key2':['one', 'two', 'one', 'two', 'one'],
        'data1':np.random.randn(5),
        'data2':np.random.randn(5)})
df['data1'].groupby(df['key1']).mean()

这里我们按照key1将数据分组,然后计算分组后的data1的平均值。

我们也可以一次传入多个数组

df['data1'].groupby([df['key1'], df['key2']]).mean()

此外,还可以将列名用作分组

df.groupby('key1').mean()
df.groupby(['key1', 'key2']).mean()

注:这里在执行df.groupby(‘key1’).mean()时,结果中没有key2列。这是因为df[‘key2’]不是数值数据,所以被从结果中排除了。默认情况下,所有数值列都会被聚合,虽然有时可能会被过滤为一个子集。

分组后可进行迭代

for name, group in df.groupby('key1'):
    print(name)
    print(group)

对于多重键的情况

for (k1, k2), group in df.groupby(['key1', 'key2']):
     print (k1, k2)
     print (group)

 上面进行的group操作返回的类型都是Series,如果我们想返回DataFrame类型的结果,那么我们可以进行如下操作

df[['data2']].groupby([df['key1']])

 

根据行类型进行分组

 groupby默认是在axis=0上进行分组的,通过设置也可以在其他任何轴上进行分组。拿上面例子中的df来说,我们可以根据行类型(dtype)对列进行分组

dict(list(df.groupby(df.dtypes,axis = 1)))

这方面还有蛮多内容,不过都还没接触过,只能暂时讲到这里了,如果要了解更多,可以访问这篇博客 ,我在后面也会逐渐完善这些方面的内容。

结尾

讲了这么多,相信你对pandas肯定有了一定的了解了,但由于个人接触不多,就暂时介绍到这里,pandas还有很多内容如数据透视表、多层索引的使用、数据清洗等,将在后面学到时再做补充