yield-bytes

沉淀、分享与无限进步

Pandas数据预处理的常用函数

  以往项目中也有引入Pandas,用于有关数据处理和分析的环节,结合Python的Web开发,很容易开发出一款轻量BI系统。Pandas、Numpy、Scipy、matplotlib、scikit-learn和Jupyter Notebook结合使用,完全可以组成非常出色的数据分析与挖掘的生产环境工具,数据方面的应用,比matlab强不少,以至于本人也不断强化这方面的积累。单独拿出这方面技能,即可完成数据分析师的相关工作(又称提数工程师)。本文主要归档一些高频使用的预处理方面的函数,注意本文不涉及Pandas数据挖掘和数理统计方面的知识点(会在另外blog给出)。

1、读取数据文件

  读取数据的相关接口使用在pandas官网的document有非常详细的说明:在IO tools部分。pandas 不仅能读取基本常用的Excel、csv、文本,还可以读取hadoop文件,或者直接读取数据库等

1.1 读取excel数据文件

  • 加载Excel表,使用skiprows=1跳过首行
    并指定加载的列,注意数据文件的编码,默认utf-8,常用还有gb2312,根据自身数据而定。

    1
    2
    3
    %%timeit
    raw_pd = pd.read_excel(data_file,,skiprows=1,usecols=[1,2,4],name=['item_id','item_name','price'],encoding='gb2312')
    181 ms ± 1.32 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

    这里可以为每个执行单元之前加入%%timeit,观察其耗时情况。

  • 加载Excel表,使用header=0跳过有列标题的首行
    除了使用skiprows=1可跳过首行,header=0也可以实现同样效果

    1
    raw_pd = pd.read_excel(data_file,header=0,usecols=[1,2,4],name=['item_id','item_name','price'],encoding='gb2312')
  • 加载Excel表,首行为数据,不是列标题
    若该表第一行不是列标题行而是数据行,则需要指定header=None,否则读取后,第一行数据会被作为column name

    1
    raw_pd=pd.read_excel(data_file,usecols=[1,2,4],name=['item_id','item_name','price'],header=None,encoding='utf-8')
  • 加载Excel表,读取前n行数据
    若数据文件大小为几个G,显然若直接全量读取,内存会挤爆,因此可以先读取前n看看。使用nrows=500,表示只读取前500行。

    1
    raw_pd=pd.read_excel(data_file,usecols=[1,2,4],name=['item_id','item_name','price'],header=None, nrows=500,encoding='utf-8')
  • 加载Excel表,跳过所有空白行
    若有些表出现了不定数量的空白行,可以使用skip_blank_lines=True处理

    1
    raw_pd=pd.read_excel(data_file,usecols=[1,2,4],name=['item_id','item_name','price'],header=None,skip_blank_lines = True, nrows=500,encoding='utf-8')
  • 加载Excel表,通过自定规则,跳过满足规则的行
    例如跳过有值为单数的行,定义更复杂的函数,用于跳过满足复杂规则的行。不过,除非这些行很多,否则可以在读取后,直接用正则drop掉来得更有效。

    1
    pd.read_csv(data_file, skiprows=lambda x: x % 2 != 0)

1.2 读取csv文件

  读取csv文件跟读取Excel文件区别不大,这里简单给出示例

1
raw_pd=pd.read_csv(data_file,usecols=[1,2,4],name=['item_id','item_name','price'],header=None,nrows=500,encoding='gb2312')

读取文件,需要注意的地方一般是选择编码,数据文件的编码决定读取数据后,是否正常显示。

1.3 读取数据时,跳过尾行

有些报表一般会在表(例如财务系统导出)的后几行写上制表人、制表日期
这里要注意,若使用c engine,则无法使用从尾部跳过数据的功能:

skipfooter : int, default 0

Number of lines at bottom of file to skip (unsupported with engine=’c’).

1
raw_pd=pd.read_csv(data_file,usecols=[1,2,4],name=['item_id','item_name','price'],header=None, skipfooter=5,encoding='gb2312')

1.4 读取特定分割符的数据文件

read_csv也可以读取任意文本文件,只需要指定列分割符。

1
raw_pd=pd.read_csv('data_file.txt',sep='||',encoding='utf-8')

1.5 使用c或者python作为读取文件的引擎

pd.read_* 方法默认使用python解释器作为读取文件engine,若数据文件大,可选择c engine

engine : {'c', 'python'}

Parser engine to use. The C engine is faster while the Python engine is currently more feature-complete.

1.6 使用迭代器读取超大文件

参考官网文档给出的示例,使用iterator=True, 或者chunksize=4读取超大文件,返回的是TextFileReader,是一个文件迭代器

chunksize方式:

1
2
3
4
5
6
7
In [187]: reader = pd.read_csv('tmp.sv', sep='|', chunksize=4)

In [188]: reader
Out[188]: <pandas.io.parsers.TextFileReader at 0x7f2b428c17f0>

In [189]: for chunk in reader:
.....: print(chunk)

iterator=True方式:
使用iterator=True方式,值读取前面5行,放回的也是df对象

1
2
3
4
In [190]: reader = pd.read_csv('tmp.sv', sep='|', iterator=True)

In [191]: chunk_pd=reader.get_chunk(5)
chunk_pd.head()

当然最佳的方式是两者结合使用:返回迭代器方式,并指定分块读取,例如分64k读取

1
iter_df=pd.read_csv(large_file,iterator=True,chunksize=64*1024)

2、查看数据的基本信息

读入数据后,一般需要对数据进行基本的观察

1
2
3
4
5
6
7
8
raw_pd.head(5) # 查看数据基本信息(前5行)
raw_pd.tail(5) # 查看末尾5行
raw_pd.sample(5) # 随机抽取5行查看
raw_pd.dtypes # 查看每列数据类型
raw_pd.columns #查看列名
raw_pd.info() #查看各字段的信息
raw_pd.shape #查看数据集行列分布,几行几列
raw_pd.describe() # 快速查看数据的基本统计信息

3、有关空值处理

空值:在pandas中的空值是””
缺失值:在dataframe中为NaN或者NaT(缺失时间),在series中为none或者nan

1
2
3
4
# 测试数据
raw_pd = pd.DataFrame({"name": ['aoo', 'boo', 'coo'],
"college": [np.nan, 'SACT', 'AACT'],
"birth_date": [pd.NaT, pd.Timestamp("2000-10-01"),pd.NaT]})

3.1 行的空值处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"""
axis:0或者'index'代表行操作(默认) 1或者'column':列操作
how:any-只要有空值就删除(默认),all-全部为空值才删除
inplace:False-返回新的数据集(默认),True-在愿数据集上操作
"""

# 使用频率高:查看name列中,有多少行为空值行,value_counts其实是一个统计方法
raw_pd['name'].isnull().value_counts()

# 使用频率高:any表示行的任意一列有空值,则删除该行;all表示该行全部为空,则删除
raw_pd.dropna(axis=0, how='any', inplace=True)


# 行的任意一列有空值,且出现2个空值才删除这些行。例如该行有3列,其中2列都是为空,那么可以删掉该行。
使用频率低:raw_pd.dropna(axis=0, how='any',thresh=2, inplace=True)

3.2 列的空值处理

1
2
# 使用频率高:指定某几列,若这些列中出现了空值,则直接删除所在行
raw_pd.dropna(subset=['name', 'birth_date'],inplace=True)

3.3 空值的填充

最简单的用法,对全部数据记录里面的空值填充指定值

df.fillna(value=’bar’)

频繁使用:对指定列的空值进行填充

1
raw_pd['name']=raw_pd['name'].fillna(value='bar')

高级填充方式
使用与空值单元相邻近的值来填充。该用法一般用在大量数据统计分析的场景或者图像的像素值填充、实验室的实验数据。相邻是指可以使用上下左右四个方向的值实现前向或者后向填充

1
2
3
4
5
6
7
8
9
10

DataFrame.fillna(value=None, method=None, axis=None, inplace=False, limit=None, downcast=None, **kwargs)

method : {‘backfill’, ‘bfill’, ‘pad’, ‘ffill’, None}, default None

Method to use for filling holes in reindexed Series pad / ffill:
propagate last valid observation forward to next valid backfill / bfill: use NEXT valid observation to fill gap

axis : {0 or ‘index’, 1 or ‘columns’}
limit:限制填充的个数

这里使用比较频繁的是纵向填充,因为纵向代表的是列,从相邻样本的同一特征中填值,对每列的空值实施前项或者后项填充。

df.fillna(method='ffill') or df.fillna(method=’bfill’)

3.4 空值使用所在列或者所在行的均值、中位数来填补

这里以均值填充为例,当然也可以用该列的预测值填充

1
2
mean_value = df['age'].mean()
df['age'].fillna(mean_value, inplace=True)

4、dataframe 取(定位)数据的操作

4.1 按给定列名取数,类似字典操作:df[‘列名’]

1
raw_pd= raw_pd['name']

取出多列数据,入参为包含多个字段的list:[‘name’,’college’]

1
raw_pd[['name','college']]

4.2 按行默认的行索引号选取数据:df.loc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
df = pd.DataFrame({"name": ['aoo', 'boo', 'coo'],
"college": [np.nan, 'SACT', 'AACT'],
"birth_date": [pd.NaT, pd.Timestamp("2000-10-01"),pd.NaT]})
# 查看该df的行索引
df.index
RangeIndex(start=0, stop=3, step=1)

# 打印
birth_date college name
0 NaT NaN aoo
1 2000-10-01 SACT boo
2 NaT AACT coo

# 这里的0,1,2就是pandas默认给加载的数据提供的行索引号

按索引取数据,跟列表使用slice切片获取数据的用法一致
df.loc[1] 获取行索引1的行数据,df.loc[1:2]获取1到2行数据

若行索引号不是int,例如将以上数据的默认index序列,改成字符索引序列

1
2
3
4
5
6
df.index=['a','b','c']
# 打印
birth_date college name
a NaT NaN aoo
b 2000-10-01 SACT boo
c NaT AACT coo

获取索引为b的数据:df.loc[‘b’]
或者索引为b、c的数据:df.loc[[‘b’,’c’]]

4.3 按给定列名以及行索引取出数据

例如取出college列的b、c行数据

1
df.loc[['b','c'],'college']

例如取出college列、birth_date列的b、c行数据

1
2
3
4
5
df.loc[['b','c'],['college','birth_date']]
# 打印
college birth_date
b SACT 2000-10-01
c AACT NaT

4.4 df.iloc利用index获取行数据或者列数据

df.iloc只能使用整型切片获取数据:例如df.iloc[0:10]
而df.loc可以使用字符型索引等来取数

5、通过复杂规则取数

在sql中经常会在where子句使用筛选条件:
select * from emp e where e.age>20 and e.dep_name=’dev’ and e.city<>’foo’
在pandas里面则使用方式如下:
单个筛选条件

1
2
3
df[df['age'] > 20]
或者
df.loc[df['age']>20]

多个筛选条件

1
df[(df['age'] > 20)&(df['dep_name']=='dev')&~(df['city']=='foo')]

使用isin方法

1
df[df['city'].isin(['foo','bar'])]

根据时间范围取值

1
2
3
4
5
6
7
8
9
10
# 从Excel加载的日期,如果格式不对,有可能是object类型,需将其转为datetime64[ns]类型,否则无法进行日期筛选比较
df['date_col']= df.to_datetime(df['date_col'])
start_time=datetime.datetime(2017,2,1) #或者pd.Timestamp('2017-02-01')
end_time=datetime.datetime(2017,2,14) #或者pd.Timestamp('2017-02-14')
# 注意以上的实际范围其实是 2017-02-01 00:00:00 ~2017-02-14 00:00:00
# 或者截止到当天最后一秒
end_time=datetime.datetime(2017,2,14,23,59,59)

# 查找指定时间范围内的数据行
filter_df=df[(df['start_time']>=start_time) & (df['end_time']<=end_time)]

还有另外这一种方式是选择一个时间列,变将其设为当前df的时间索引,

常用:根据某个字段,取出获取删除其值出现频率排在前n的数据行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#  对name字段进行group_by
groupby_df=df.groupby('name')
# groupby_df:<pandas.core.groupby.DataFrameGroupBy object at 0x0000000010190C18>

resl_df=groupby_df['name'].count()
# resl_df 就像透视表的数据形式
name
foo 10
bar 5
coo 4
dee 2
Name: bar, dtype: int64

# 找name字段里,字符出现频率排在前2位,例如上述的例子:foo,bar。按降序返回一个python的列表
top_2_list=resl_df.sort_values(ascending=False).head(2)
print(top_2_list)
['foo','bar']

# 将出现频率排在前2的内容拼接成用于正则匹配的字符串
pattern_str='|'.join(top_2_list)

# 使用pandas提供的正则方法,剔除name字段中出现频率排在前2的数据行
filtered_df= df[~df['name'].str.contains(pattern_str, case=False, na=False,regex=True)]

==使用时间索引选取数据行==
个人认为,这种方式是时间选取数据场景最高效的手段
例如有数据df,其中create_date是该df唯一的日期字段,通常做法:
新增一列命名为back_up_date,用于备份create_date
df['back_up_date']=df['create_date']
将crete_date置为该df的时间索引
df=df.set_index('create_date')
当时间索引设置后,那么根据时间筛选数据将变得异常简单
取2000年的数据行
df['2000']
取2000年到2019年的数据行
df['2000':'2019']
某天具体时间到某天具体时间的数据行
df['2015-03-15 11:11:10':'2015-05-15 10:30:00']
有关pandas时间的专题,官方文档给出了非常详细的用法示例,这里不再赘述,timeseries链接

6、调整列位置、列的增、删

交换birth_date和college列的位置

1
df[['birth_date','college']]=df[['college','birth_date']]

删除指定列

1
df.drop(columns=['B', 'C'])

删除指定行,使用行索引

1
df.drop([0, 1])

删除重复行:df.drop_duplicates

直接删除重复行:

1
df.drop_duplicates()

删除name列、age列存在重复的行

1
df.drop_duplicates(['name','age'],keep='first',inplace=True)

请注意:以上删除行的操作,会破坏df原有的0到n的连续索引,例如原行索引为:0,1,2,3…n,其中索引1,2为空行,删除空行后,df的索引变为0,3…n,显然不连续,因此需要重置索引:df.reset_index(drop=True),重置后,索引变为0,1,2,3…n

7、单元格的字符相关处理

例如有字段app_id,有部分值字符串为数字:‘12331’,需转成int64
有部分值为字符加数字:‘TD12331’,去掉字符TD并转成int64
有些值为非id例如:‘ # llsd’,需对此类值用固定数值填充。因此需要对其统一处理成整型id
使用replace方法去掉值里面的TD字符串

1
df['app_id'].replace('TD','',regex=True,inplace=True)

使用apply方法通过定义简单的lambda去掉值里面的TD字符串

1
df['app_id']=df['app_id'].apply(lambda:item:item.replace('TD',''))

其实apply才是终极方法,适用各种自定义的数据行或者列的处理,例如对同一列的值有多种判断需要处理,则可以在外部定义要处理的函数,再用apply广播到该列的每个cell中。例如上面的例子:如果单元格数值含有TD则去掉TD字符保留其数值部分,如果单元格出现非数值,则将其设为NaN空值

1
2
3
4
5
6
7
8
9
def filter_id(cell):
if re.match('\d{3}',cell):
return cell
elif re.match('TD',cell):
return re.sub('TD','',cell)
else:
return np.nan

df['app_id']=df['app_id'].apply(filter_id)

apply方法另外一种常用的方式:对数值进行分级,例如10<item<30为D,30<=item<60为C,60<=item<90为B。此外,货币进行转化、时间转换也是常用的场景

1
2
3
4
5
6
7
8
9
10
11
def level(item):
if 10<=item<30:
return 'D'
elif 30<=item<60:
return 'C'
elif 60<=item<90:
return 'B'
else:
return 'A'

df['level']=df['level'].apply(level)

使用astype转成整型id号,具体其他数据类型不再列出。astype要求整列数据是完整的同一数据类型

1
df['app_id']=df['app_id'].astype('int64')

使用频繁:使用pandas.Series.str.contains方法处理列的值

Series.str.contains(pat, case=True, flags=0, na=nan, regex=True)
pat : str
Character sequence or regular expression.
case : bool, default True
If True, case sensitive.
flags : int, default 0 (no flags)
Flags to pass through to the re module, e.g. re.IGNORECASE.
na : default NaN
Fill value for missing values.
regex : bool, default True
If True, assumes the pat is a regular expression.
If False, treats the pat as a literal string.

删除name列中含有foo字符串的行,默认使用正则匹配
df=df[~df[‘name’].str.contains(‘foo’, case=false, flags=re.IGNORECASE, na=False)]

或者使用正则匹配
df=df[~df[‘name’].str.contains(‘^foo’, case=false, flags=re.IGNORECASE, na=False)]

8、有关遍历行的处理

单表处理,请勿使用循环!! 效率很低!有apply方法足以,底层是矩阵操作。

遍历行,一般用在两个表之间,表A字段’date’与表B字段’date‘的比较
使用iterrows遍历行
iterate over DataFrame rows as (index, Series) pairs.

1
2
3
4
5
6
7
# 这种方式可以把索引和行数据遍历出,其中row的数据结构为nametuple
for index,row in df.iterrows():
print(index,row)

# 这种方式其实就是itertuples(index=False)的遍历
for _,row in df.iterrows():
print(row.A,row.B)

使用itertuples遍历行
Iterate over DataFrame rows as namedtuples of the values.

1
2
3
4
5
6
s = pd.Series(pd.date_range('2012-1-1', periods=10, freq='D'))
td = pd.Series([pd.Timedelta(days=i) for i in range(10)])
df = pd.DataFrame({'A': s, 'B': td})
#这种方式,取出的每行为nametuple
for row in df.itertuples(index=False):
print(row.A,row.B)

使用iteritems遍历列
这种方式以横向遍历列数据,每次返回该列名和该列Series

9、DataFrame和DataFrame合并、关联查询等

9.1 DataFrame和DataFrame合并

合并具有相同结构的df
将多个DataFrame按垂直方向或者水平方向合并:这种场合使用批量处理具有相同字段结构的多份报表或数据源

1
2
3
4
5
6
7
8
9
# 默认是按垂直方向合并三个子df
frames = [df1, df2, df3]
result = pd.concat(frames)

# 在合并后,还可以为每个子df设定相应key
result = pd.concat(frames, keys=['foo', 'bar', 'cee'])

# 利用上面key,可以一次性取回合并前的df1
df1=result.loc['foo']

合并字段不同的df

9.2 DataFrame和DataFrame之间的关联查询

因为关联查询基本是数据分析里面重要的、使用频繁的需求,例如实现报表1和报表的用vlookup关联查询、sql中多个表的关联查询(内连接、左连接、右连接、全连接)。pandas的doc官方文档在这部分的内容已经非常详细,并且有相应的关联前后的图文说明,本文不再一一赘述,仅给出简单的关联用法。
以内连接为例:
实现类似sql使用两表的多个外键关联:

1
2
3
select t1.*,t2.* from t1,t2 where t1.a=t2.a
and t1.b=t2.b
and t1.c=t2.c

pandas的方式
1
df1.merge(df2, on=[ key1 ,  key2 ,  key ])

使用单个字段(外键)关联两表
1
df1.merge(df2, on='dept_id')

10、groupby基本用法

groupby可以说面对不同的数据需求,有不同用法,对sql熟悉的人应该无需多说。这里仅给出一些简单用法。

按季度分组,提取每个分组前n个数据行

1
2
3
4
def top_n(df,n=3):
return df[0:n]
# 这里的n是top_n自定义的关键字参数n,不是apply的参数
df.groupby('quarter').apply(top_n,n=3)

按产品种类分组,提取每个分组里最大值和最小值之差

1
2
3
4
# 每个产品种类的数值跨度范围,也即最大值减去最小值
def max_min(item):
return item.max() - item.min()
df.groupby('prod').agg(max_min)

按产品种类分组,一次性取出每组的最值、均值、数值跨度范围,这里需要注意agg的入参为方法的列表,内置方法使用其字符名,自定义方使用其函数名

1
df.groupby('prod').agg(['mean','max','min',max_min])