大家好!好久不见!适逢国庆,先祝我亲爱的祖国七十周年生日快乐!
这一系列文章是针对最近在实习中做的一些数据预处理操作的整理。我们希望通过它们,带领大家了解和熟悉一些python做数据清洗,数据整合等的常见操作。
请注意我们这是专题文章,也就是说这一系列文章并不假设你对python是零基础的。但是如果你和我一样,也是一个数据科学中探索的小白,那么我希望你也能够通过这些文章,在一个一个的数据处理的坑中爬起来,总结经验,奋勇前行~
写这一系列文章之前我也咨询了老板的意见,也得到了他的同意。说这句话的意思是:所有这里涉及的操作,在数据分析中都是基础,重要和容易踩坑的。所以即使你现在觉得它们难以理解,或者用不上,以后也大概率会碰到。还需要和大家说的是,为了防止泄露商业机密和业务材料,我们的数据集事先做过处理。
那么我们开始吧。没有特殊介绍,我们都是使用Python3的。
虽然这非常简单,但我们还是提一下。凡是做数据预处理,都不可避免的用上这两个包。
import numpy as np
import pandas as pd
如果你要做基本的可视化,那还需要这个包
import matplotlib.pyplot as plt
这句语法的意思就是:导入...包,并给它命名为np/pd。很多人经常在代码中看到诸如np., pd. 这样的前缀,意思就是调用numpy/pandas内的函数(还有一个说法叫方法)。
网络上有非常多关于这两个包的命令详解,百度一下都有。至于如何安装,各位只需要百度一下anaconda即可。anaconda本身就可以理解为是一个封装好的python环境,里面已经事先有安装好大部分必备的库。
当然,你也可以使用
pip install ... # ... is the name of package
有一个情况是你电脑里的默认python不是anaconda内的那个(当你电脑里装了不止一个py的时候,这种情况是完全有可能的)。除了环境变量的优先级调整法(具体的可以百度),还有一种方法我觉得更方便,就是打开anaconda的prompt。
这个Prompt可以理解为anaconda内自己的cmd (command line)。那么这个时候,下面这一行代码
就可以自动把你想要安装的包安装到你的anaconda中。对于我这种py底层机理一窍不通的人来说,这已经够用了,也很方便。虽然大部分情况都可以通过anaconda的可视化界面完成,但是我就碰到了tensorflow装不上的情况,这个时候就只能采用上面那种方法了。
配置有了,下一步自然是读取数据,创建dataframe了。dataframe是pandas特有的一种数据结构,通俗理解就是一种计算机中存储数据的结构,好吧这好像是一句废话……
我截取了一部分我司的数据集(很显然不可能是真实的,我在上面说过了),我们希望用它作为我们的例子,那么很显然就需要构造一个dataframe来存储它。常见的操作就是先构造一个字典,再转换。具体如下
Table = {
'date': ['2019/6/1', '2019/7/2', '2019/6/6', '2019/6/17', '2019/7/4',
'2019/6/13', '2019/6/14', '2019/6/21', '2019/6/17'], 'order_id': [i+1 for i in range(9)],
'commodity_code': ['S1', 'S2', 'S3', 'S5', 'S5', 'S2', 'S9', 'S11', 'S9'], 'commodity_name': ['标准美式',
'瑞纳冰', '加浓美式', '拿铁', '拿铁', '瑞纳冰', '菠萝卷', '坚果', '菠萝卷'], 'category_name': ['饮品',
'饮品', '饮品', '饮品', '饮品', '饮品', '食品', '食品', '食品']}
Table = pd.DataFrame(Table)
如果你运行正常的话,最后应该可以看到这样的结果。
(这是在Jupyter Notebook上运行的,如果你在别的IDE上运行,显示的样子可能会有些不一样)相信通过这个例子你也大概明白了什么是一个dataframe。
注意:代码pd.DataFrame()中是首字母大写的。
注意:你也许看到了这么一行代码
[i+1 for i in range(9)]
这是列表生成式。比方说它的目的就是生成1-9的一个列表。如果把它拆分一下,就是这么一个意思
A = []
for i in range(9):
A.append(i+1)
append方法就是在已有的列表之后再尾插一个元素。range是一个迭代器,具体用法百度。
很显然我们不会一直这么录入数据的,实际的数据集一般都是很大的。这个时候一般都会读取已有的大的.csv格式的数据集。一般使用
Table = pd.read_csv("file.csv", encoding = 'utf-8', sep = ',', error_bad_lines = False)
比方说如果要读取上面的那个数据集,那么你的csv文件用excel打开应该长这样
.csv文件本质上是用逗号分隔的文本文件,所以没有xls/xlsx文件那么大。当然了很多格式也无法保存在.csv文件中(比方说对一个数据标红,加粗等,这些格式都无法保存)。如果你用记事本打开.csv,文件是长这样的,朴实而无华。
那么多逗号是因为我手动删除了很多列……
最后说几个参数
在介绍下一个环节之前,我需要提一下的是我们的所有数据分析都会在本地进行。关于大数据架构的相关内容短时间并不会提到。
虽然我们的数据非常小,但是实际情况下,数据因为过大很多时候在下载,提取的时候都会占用巨大的时间,因此很多时候我们都会分批次的下载。这种情况下我们自然需要把数据“拼”起来。如何去做呢?
Temp = [Table1, Table2]
Table = pd.concat(Temp, axis = 0)
pd.concat方法中的axis参数标志的是行和列。比方说这里axis=0意思就是按行拼接,axis=1就是按列拼接了。当然了,事先你需要把一些该拼起来的dataframe放在一个列表里。自然你还需要注意的是如果要按行拼接,那么自然两个dataframe的列名,列数都是要相同的。另外一个同理。
当然,如果你是纯拼接的,这样子当然OK,但是很多时候,出现的情况是两个待拼接的矩阵的列名有相同的地方。我们经常会使用SQL提取我们想要的特征,不可能一个原始的表就能提供我们想要的所有信息,这个时候要在不同的数据表中提取,就会涉及到左连接,右连接,内连接和外连接的情况。考虑到它们只是差别一个参数how,所以我们只解释这四个名词,就不举例子了。
Definition 1: Left/Right/Inner/Outer Join
左连接:表A, B连接只保留左表A的全部信息
右连接:表A, B连接只保留右表B的全部信息
内连接:表A, B连接只保留两表重合的信息
外连接:表A, B连接,除两表重合的信息外,列按序全部保留
详细的可以参考下面这篇文章
Pandas DataFrame连接表,几种连接方法的对比www.jianshu.com如果对几个简单的命令不熟悉,那么这也是一个脏活。这里我们单独举几个例子。
首先来看简单列表情况的。
Example 1:
找出列表中大于等于3的数据,不改变表元素的顺序
学过循环的人马上开怀大笑,简单!
B = []
for i in range(len(A)):
if A[i] >= 3:
B.append(A[i])
B
是,确实是一个不难的题目,可是Python有一个非常大的忌讳就是使用for循环。因为py的循环运行的很慢,所以实际我们的代码中会尽量避免写for循环(一般最多写一重循环,二重的说什么也得改成一重2333),所以如果数据量大一点,到了GB, TB级别的,这代码就该傻眼了。之前我们说的列表生成式其实本质上也是一种取代for循环的一个trick。
事实上,python有很多骚操作可以做一样的事情。
A = [3, 2, 1, 2, 4, 6, 3]
list(filter(lambda x: x >= 3, A))
# Result: [3, 4, 6, 3]
filter函数就可以完成这个任务,需要说明的是匿名函数lambda。你可以理解为filter就是把匿名函数施加到了每一个列表的元素中,达到“筛选”的目的。
如果是针对dataframe呢?我们用上面那个列表举例子。
A = pd.DataFrame(A)
A[(A >= 3).values.tolist()]
'''
Result:
0
0 3
4 4
5 6
6 3
'''
很多人可能看不明白这个操作,我们看一下拆解代码后的结果。
A >= 3
'''
Result:
0
0 True
1 False
2 False
3 False
4 True
5 True
6 True
'''
(A >= 3).values.tolist() # Result: [[True], [False], [False], [False], [True], [True], [True]]
也就是说,我们做的事情就是先通过逻辑表达式判断T/F,然后用T/F去做筛选。
这样子的话,又一个问题来了,我可不可以直接这样做。
A[A >= 3]
虽然R语言是可以的,但是Python里面这样做是需要一些预处理的。需要注意的是,这个操作如果针对的是pandas里的Series结构,那么是没有问题的。但是针对dataframe是8行的……好,那么我们可不可以自己造一个Series呢?
A = pd.Series(A, index = [i for i in range(len(A))])
A[A>=3]
'''
Result:
0 3
4 4
5 6
6 3
dtype: int64
'''
其实Series就是一个仅有一列的dataframe(但是严格意义上来说这是不对的,两个数据结构之间的方法,操作等是泾渭分明的)。
注意:一个很自然的联想是,如果我希望选取一个区间,也就是说我需要两个条件来制约。比方说我希望寻找值在[3, 4]的元素,那么有的人可能会写成
A[A >= 3 & A <= 4]
但事实上这是会报错的。真正的写法应该是这样
A1 = A >= 3
A2 = A <= 4
A[A1 & A2]
这个谜题至今我不知道为什么……征个解了,希望出没在评论区的大佬可以解答~
至于提到针对Series结构不太一样的原因,我们之后再提,先放在这里。
除了这样的逻辑关系筛选,其实还有一种筛选就是定点筛选。比方说下面这个业务需求。
Example 2:
在A这个dataframe中挑选出值为2和3的所有元素。
注意这里的“值”的意思就是value,也就是除了index(最左边那一列)外的其他列。
我们如何实现它呢?
A = pd.DataFrame(A)
A[A[0].isin([2, 3])]
'''
Result:
0
0 3
1 2
3 2
6 3
'''
这里的.isin方法就需要我们提供一个列表,那么只要元素是这列表中的其中一个,就会被筛选出来。这个命令很多时候会很重要,比方说在推荐系统中,我们可能需要筛选出用户中的高频用户,那么就可以通过逻辑关系筛选出这些用户的ID,然后把ID的列表导出来,用这个列表去过滤原始的数据集,筛选出他们的销售数据。
注意:你也许注意到了这个
A[0]
这个索引并没有数量意义,这仅仅是因为A这个dataframe中,我们需要的那一列数据它的索引名是0(因为我们是直接把列表转为了dataframe,所以默认索引为0)。也许你会想知道,那如果我希望知道dataframe的所有列名,怎么做呢?
A.columns.values.tolist()
其实最重要的就是.columns这一个方法,其它的都是为了好看2333。另外这个也是只针对dataframe的(因为series是没有header,也就是列名的)
你看,即使就是一个简单的筛选,还有这么多细节的操作。
这一个部分主要是涉及到两个操作,我们一个一个来看。
首先这第一个,其实是从这个业务需求来的。
Example 3:
找出高频用户
你可能会想,这不是刚才已经提到过了吗?请注意我们是在业界,而业界的数据库中往往并没有直接提供你算法所使用的数据集。比方说在我司,能够知道的就只有按照时间排列的每一笔的销售数据,但是如果你需要找到个人购买量,那就只能靠我们的整合代码了。
我们显然不会直接动用我司的数据集来演示。别忘了我们在第一个部分给大家手动创建了一个小的dataframe,我们就使用那个吧(变量名为Table)。
Table.groupby(['commodity_code']).count()
它的运行结果如下
这里我们用到的是groupby方法,也就是分组聚合。如果我们前面的列表生成式是对于每一个列表中的元素施加一个操作的话,这里的groupby就是针对dataframe中所选的那一列为“组名”来分组施加运算。比方说我们在原始的数据中,发现有2个人买了代号为S5的商品,那么你就可以看到,这两个人就会对应两个订单编号,两个日期,等等,而count方法就是计数的,所以这一行都是2。
就这些了吗?闹太套!我不要你觉得,我要我觉得,这个问题不需要商量,都听我的,完全不止这些!比方说下面这个
Table.groupby(['commodity_code'])['commodity_code'].sum()
'''
Result:
commodity_code
S1 S1
S11 S11
S2 S2S2
S3 S3
S5 S5S5
S9 S9S9
Name: commodity_code, dtype: object
'''
注意:这一行代码有两个点可以说:
所以我们可以把筛选与整合结合起来,就可以完成我们下面这个业务需求了。
Example 4:
过滤出购买超过一单的人对应的所有订单信息
我们还是使用Table这个数据集。
Temp = Table.groupby(['commodity_code'])['commodity_code'].count()
B = Temp[Temp >= 2]
C = B.index
Result = Table.loc[Table['commodity_code'].isin(C)]
虽然看上去四行代码就完成我们的任务了,可是对于不熟练的人(比方说我),可能得摸索好几天呢……
注意:如果你真的一步一步按照操作完成了,你会发现新的这个数据集它的索引并没有变化,这样的话如果你希望为数据添加特征(也就是添加几列),可能会因为索引不匹配而报错。所以一般推荐再加一行。
Result.reset_index(drop = True)
这样就会重新从0开始按序排列索引。当然如果你把drop参数设置成了F,那么原始的索引会被作为一列值,其列名为index,新的索引依然是从0开始。
说完groupby我们来说一下透视表。透视表其实可以理解为一种双向groupby。也就是说,它相当于选取了数据集的两个特征做分组聚合。然后把这两个特征的值作为两个坐标轴。具体的我们可以参考Table数据集的这个例子。
Example 5:
构造出一个以大分类为列,小分类为行的销量数据。
它的代码是这样的
Result = pd.pivot_table(Table, index = 'category_name', columns = 'commodity_code', aggfunc = np.sum, fill_value = 0)
'''
Result:
order_id
commodity_code S1 S11 S2 S3 S5 S9
category_name
食品 0 8 0 0 0 16
饮品 1 0 8 3 9 0
'''
pivot_table方法就是规定一列为新矩阵的列(index),一行为新矩阵的行(columns)。看似行列都有,结构完整,可是值是不是有点问题?你看最上方那个order_id也能明白发生了什么,是吧?没有错,使用透视表的时候,如果使用np.sum,那么它会默认选择一个数类别(比方说int)的列进行操作。可能有的人会觉得可不可以使用np.count/pd.count,可是人家没有这个函数啊……
我自己的解决方法是多拼接一列全部为1的数,然后再指定求和针对这一列。
Col = np.tile(1, (len(Table), 1))
Col = pd.DataFrame(Col)
Col.columns = ['Count']
Obj = [Table, Col]
Table = pd.concat(Obj, axis = 1)
Result = pd.pivot_table(Table, values = 'Count', index = 'category_name', columns = 'commodity_code',
aggfunc = np.sum, fill_value = 0)
'''
Result:
commodity_code S1 S11 S2 S3 S5 S9
category_name
食品 0 1 0 0 0 2
饮品 1 0 2 1 2 0
'''
可以看出来我们做了两个改动,除了刚才说的拼接一列以外,还有一个就是我们多指定了函数中的values参数,这就可以指定在哪一列进行操作。你可以看到,新的数据集中值均正确,也没有多出诡异的一个头。
注意:pd.concat只可以拼接dataframe,所以如果是numpy的array,就需要做一步转换了。为了方便我们索引,我们给它加了一个列名(.columns方法)
值得一提的函数是np.tile,它的机理是对已有的列表,矩阵进行不同轴上的复制粘贴。举个例子,生成一个全1的长度为10列向量,其本质就是一个1,然后按列方向复制粘贴了10次,对吧?比方说下面这个例子
A = np.array([1,2,3,4]).reshape(2,2)
np.tile(A, (2, 2))
'''
Result:
array([[1, 2, 1, 2],
[3, 4, 3, 4],
[1, 2, 1, 2],
[3, 4, 3, 4]])
'''
你也可以看到,在numpy中构造矩阵的方式就是先构造一个向量,然后用reshape方法去重塑它的结构。这里np.tile这相当于一个小矩阵行列各扩充为原来的两倍。感兴趣的人可以试试把它转为dataframe,看看它的索引,列名是什么?
最后关于透视表的其它操作可以参考这个官方文档。
pandas.DataFrame.pivot_table - pandas 0.25.1 documentationpandas.pydata.org如果仅仅是说选取一个区间内的子集,这个我相信没必要单独拉出来说,直接用pandas内的数量索引就好。但是如果希望删去指定行列呢?比方说刚才那个Table数据集,我们希望把那个多余的全1向量删去,怎么办?
Table = Table.drop('Count', axis = 1)
这样就可以复原了。注意这里axis = 1表示考虑列名,而不是索引。
显然,我们还需要把我们做好处理的文件再导出为一个csv,这在py中也不难做到。
Result.to_csv('File.csv', sep='t', header=True, index=True)
header和index就是表头(列名)和索引,如果均选择T,那么就会显示出它们,否则就只会显示出这些值所构成的矩阵了。
注意:在保存文件的时候要注意你选择的工作路径。因为如果路径不是绝对路径,那么保存的时候就会保存在你的工作路径下。
在这一节我们主要介绍了py中一些相对比较常见,但是又有一些小难度的实际数据分析中需要使用的python操作集合。所有的总结均是个人摸索的操作,因此具有很强的主观性。我非常希望能够有py大佬在评论区,或者私信,对不同的业务需求提出更好的解决方案!也希望和我一样的数据分析新手能够从中受益~
下一篇笔记传送门:Python|专题(2)——数据处理常规操作集(2),数值计算的几个加速技巧
——————————————————————————————————————
本专栏为我的个人专栏,也是我学习笔记的主要生产地。任何笔记都具有著作权,不可随意转载和剽窃。
个人微信公众号:cha-diary,你可以通过它来获得最新文章更新的通知。
《一个大学生的日常笔记》专栏目录:笔记专栏|目录
《GetDataWet》专栏目录:GetDataWet|目录
想要更多方面的知识分享吗?可以关注专栏:一个大学生的日常笔记。你既可以在那里找到通俗易懂的数学,也可以找到一些杂谈和闲聊。也可以关注专栏:GetDataWet,看看在大数据的世界中,一个人的心路历程。我鼓励和我相似的同志们投稿于此,增加专栏的多元性,让更多相似的求知者受益~