目录
数据预处理
数据读取
DataFrame结构
数据索引
创建DataFrame
Series操作
数据分析
统计分析
pivot数据透视表
groupby操作
常用函数操作
Merge操作
排序操作
缺失值处理
apply自定义函数
时间操作
绘图操作
大数据处理技巧
数值类型转换
属性类型转换
数据预处理
数据读取
虽然在Excel表中也可以轻松地打开数据,但是操作起来比较麻烦,所以接下来的工作就全部交给 Pandas了,首先把数据加载进来:
df=pd.read_csv('./data/titanic.csv')
df.head() #展示读取数据,默认是前5条
需要指定好数据的路径,至于read_csv()函数,从其名字就可以看出其默认读取的数据格式是.csv,也 就是以逗号为分隔符的。其中可以设置的参数非常多,也可以自己定义分隔符,给每列数据指定名字。
如果想展示更多的数据,则可以在head()函数中指定数值,例如df.head(10)表示展示其中前10条数 据,也可以展示最后几条数据。
df.tail() #默认展示最后五条
DataFrame结构
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 PassengerId 891 non-null int64
1 Survived 891 non-null int64
2 Pclass 891 non-null int64
3 Name 891 non-null object
4 Sex 891 non-null object
5 Age 714 non-null float64
6 SibSp 891 non-null int64
7 Parch 891 non-null int64
8 Ticket 891 non-null object
9 Fare 891 non-null float64
10 Cabin 204 non-null object
11 Embarked 889 non-null object
dtypes: float64(2), int64(5), object(5)
memory usage: 83.7+ KB
可以看到,首先打印出来的是pandas.core.frame.DataFrame,表示当前得到结果的格式是DataFrame, 看起来比较难以理解,暂且把它当作是一个二维矩阵结构就好,其中,行表示数据样本,列表示每一个特征指标。基本上读取数据返回的都是DataFrame结构,接下来的函数讲解就是对DataFrame执行各种常用操作。
df.info()函数用于打印当前读取数据的部分信息,包括数据样本规模、每列特征类型与个数、整体的内存占用等。
注意:通常读取数据之后都习惯用.info()看一看其基本信息,以对数据有一个整体印象。
DataFrame能调用的属性还有很多,下面列举几种,如果大家想详细了解每一种用法,则可以参考其 API文档:
df.index # 返回索引
RangeIndex(start=0, stop=891, step=1)
df.columns # 拿到每一列特征的名字
Index(['PassengerId', 'Survived', 'Pclass', 'Name', 'Sex', 'Age', 'SibSp',
'Parch', 'Ticket', 'Fare', 'Cabin', 'Embarked'],
dtype='object')
df.dtypes#每一列的类型,其中object表示python中的字符串
PassengerId int64
Survived int64
Pclass int64
Name object
Sex object
Age float64
SibSp int64
Parch int64
Ticket object
Fare float64
Cabin object
Embarked object
dtype: object
df.values#直接读取数值矩阵
array([[1, 0, 3, ..., 7.25, nan, 'S'],
[2, 1, 1, ..., 71.2833, 'C85', 'C'],
[3, 1, 3, ..., 7.925, nan, 'S'],
...,
[889, 0, 3, ..., 23.45, nan, 'S'],
[890, 1, 1, ..., 30.0, 'C148', 'C'],
[891, 0, 3, ..., 7.75, nan, 'Q']], dtype=object)
数据索引
在数据分析过程中,如果想取其中某一列指标,以前可能会用到列索引,现在更方便了——指定名字即可:
age=df['Age']
age[:5]
0 22.0
1 38.0
2 26.0
3 35.0
4 35.0
Name: Age, dtype: float64
在DataFrame中可以直接选择数据的列名,在读取数据时,read_csv()函数会默认把读取数据中的第一行当作列名,大家也可以打开csv文件观察一下。
如果想对其中的数值进行操作,则可以把其结果单独拿出来:
age.values[:5]
array([22., 38., 26., 35., 35.])
读取完数据之后,最左侧会加入一列数字,这些在原始数据中是没有的,相当于给样本加上索引
默认情况下都是用数字来作为索引,但是这份数据中已经有乘客的姓名信息,可以将姓名设置为索 引,也可以自己设置其他索引。
df=df.set_index('Name')
df.head()
此时索引就变成每一个乘客的姓名。
如果想得到某个乘客的特征信息,可以直接通过姓名来查找。
age=df['Age']
age['Allen, Mr. William Henry']
35.0
如果要通过索引来取某一部分具体数据,最直接的方法就是告诉它取哪列的哪些数据:
df[['Age','Fare']][:5]
Pandas在索引中还有两个特别的函数用来帮忙找数据。
(1).iloc():用位置找数据
df.iloc[0] #拿到第一个数据,索引依旧从开始
PassengerId 1
Survived 0
Pclass 3
Name Braund, Mr. Owen Harris
Sex male
Age 22.0
SibSp 1
Parch 0
Ticket A/5 21171
Fare 7.25
Cabin NaN
Embarked S
Name: 0, dtype: object
df.iloc[0:5] # 也可以使用切片来拿到一部分数据
df.iloc[0:5,1:3] #不仅可以指定样本,还可以指定特征
(2) .loc():用标签找数据。如果使用loc()操作,还可以玩得更个性一些。
df=df.set_index('Name')
df.loc['Heikkinen, Miss. Laina'] # 直接通过名字来取数据
PassengerId 3
Survived 1
Pclass 3
Sex female
Age 26.0
SibSp 0
Parch 0
Ticket STON/O2. 3101282
Fare 7.925
Cabin NaN
Embarked S
Name: Heikkinen, Miss. Laina, dtype: object
df.loc['Heikkinen, Miss. Laina','Fare']
7.925
df.loc['Heikkinen, Miss. Laina':'Allen, Mr. William Henry',:]
如果要对数据进行赋值,操作也是一样的,找到它然后赋值即可:
df.loc['Heikkinen, Miss. Laina','Fare']=1000
在Pandas中bool类型同样可以当作索引:
df['Fare']>40 # 选择船票价格大于40的乘客
Name
Braund, Mr. Owen Harris False
Cumings, Mrs. John Bradley (Florence Briggs Thayer) True
Heikkinen, Miss. Laina True
Futrelle, Mrs. Jacques Heath (Lily May Peel) True
Allen, Mr. William Henry False
...
Montvila, Rev. Juozas False
Graham, Miss. Margaret Edith False
Johnston, Miss. Catherine Helen "Carrie" False
Behr, Mr. Karl Howell False
Dooley, Mr. Patrick False
Name: Fare, Length: 891, dtype: bool
df[df['Fare']>40][:5] # 通过bool类型来筛选船票价格大于0的乘客并展示前5条
df[df['Sex']=='male'][:5] # 选择乘客性别是男性的所有数据
df.loc[df['Sex']=='male','Age'].mean() # 计算所有男性乘客的平均年龄
30.72664459161148
(df['Age']>70).sum() # 计算大于70岁乘客的人数
5
创建DataFrame
DataFrame是通过读取数据得到的,如果想展示某些信息,也可以自己创建:
data={'country':['China','America','India'],'population':[14,3,12]}
df_data=pd.DataFrame(data)
df_data
下面来看几个常用的设置:
pd.get_option('display.max_rows') # 显示当前设置的参数
60
pd.set_option('display.max_rows',6) #把最大显示限制成6
pd.Series(index=range(0,100)) #Series相当于二维数据中某一行或一列
0 NaN
1 NaN
2 NaN
..
97 NaN
98 NaN
99 NaN
Length: 100, dtype: float64
由于设置了display.max_rows=6,因此只显示其中6条数据,其余省略了。
pd.get_option('display.max_columns') # 默认最大显示的列数
20
如果数据特征稍微有点多,可以设置得更大一些:
pd.set_option('display.max_columns',30)
pd.DataFrame(columns=range(0,30))
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
Series操作
读取的数据都是二维的, 也就是DataFrame;如果在数据中单独取某列数据,那就是Series格式了,相当于DataFrame是由Series组合起来得到的。
创建Series
data=[10,11,12]
index=['a','b','c']
s=pd.Series(data=data,index=index)
s
a 10
b 11
c 12
dtype: int64
其索引操作(查操作)也是完全相同的:
s.loc['b']
11
s.iloc[1]
11
再来看看改操作:
s1=s.copy()
s1['a']=100
s1
a 100
b 11
c 12
dtype: int64
也可以使用replace()函数:
s1.replace(to_replace=100,value=101,inplace=True)
s1
a 101
b 11
c 12
dtype: int64
replace()函数的参数中多了一项inplace,如果设置inplace=False,就是不将结果赋值给变量,只相当于打印操作;如果设置inplace=True,就是直接在数据中执行实际变换,而不仅是打印操作.
不仅可以改数值,还可以改索引:
s1.index
Index(['a', 'b', 'c'], dtype='object')
s1.index=['a','b','d']
s1.index
Index(['a', 'b', 'd'], dtype='object')
可以看到索引发生了改变,但是这种方法是按顺序来的,在实际数据中总不能一个个写出来吧?还 可以用rename()函数,这样变换就清晰多了.
s1.rename(index={'a':'A'},inplace=True)
s1
A 101
b 11
d 12
dtype: int64
接下来就是增操作了:
data=[100,110]
index=['h','k']
s2=pd.Series(data=data,index=index)
s3=s1._append(s2)
s3['j']=500
s3
A 101
b 11
d 12
h 100
k 110
j 500
dtype: int64
增操作既可以把之前的数据增加进来,也可以增加新创建的数据。但是感觉增加完数据之后,索引 有点怪怪的,既然数据重新组合到一起了,也应该把索引重新制作一下,可以在_append函数中指定 ignore_index=True参数来重新设置索引.
最后还剩下删操作,最简单的方法是直接del选中数据的索引:
del s1['A']
s1.drop(['b','d'],inplace=True)
给定索引就可以把这条数据删除,也可以直接删除整列,方法相同.
数据分析
统计分析
拿到特征之后可以分析的指标比较多,例如均值、最大值、最小值等均可以直接调用其属性获得。 先用字典结构创建一个简单的DataFrame,既可以传入数据,也可以指定索引和列名:
df=pd.DataFrame([[1,2,3],[4,5,6]],index=['a','b'],columns=['A','B','C'])
df
A B C
a 1 2 3
b 4 5 6
df.sum() #默认是对每列计算所有样本操作结果,相当于df.sum(axis=0)
A 5
B 7
C 9
dtype: int64
df.sum(axis=1) # 也可以指定维度设置计算方法
a 6
b 15
dtype: int64
同理均值df.mean()、中位数df.median()、最大值df.max()、最小值df.min()等操作的计算方式都相同。 这些基本的统计指标都可以一个个来分析,但是还有一个更方便的函数能观察所有样本的情况:
df.describe()
A B C
count 2.00000 2.00000 2.00000
mean 2.50000 3.50000 4.50000
std 2.12132 2.12132 2.12132
... ... ... ...
50% 2.50000 3.50000 4.50000
75% 3.25000 4.25000 5.25000
max 4.00000 5.00000 6.00000
读取完数据之后使用describe()函数,既可以得到各项统计指标,也可以观察数据是否存在 问题,例如年龄的最小值是否存在负数,数据是否存在缺失值等。实际处理的数据不一定完全正确,可能会存在各种问题。 除了可以执行这些基本计算,还可以统计二元属性,例如协方差、相关系数等,这些都是数据分析中重要的指标:
df.cov() # 协方差矩阵
df.corr() #相关系数
如果还想统计某一列各个属性的比例情况,例如乘客中有多少男性、多少女性,这时候 value_counts()函数就可以发挥作用了:
df['Sex'].value_counts() # 统计该列所有属性的个数
Sex
male 577
female 314
Name: count, dtype: int64
df['Sex'].value_counts(ascending=True) # 指定顺序,让少的排在前面
Sex
female 314
male 577
Name: count, dtype: int64
df['Age'].value_counts(ascending=True)
#如果对年龄这种非离散型指标就不太好弄
Age
74.0 1
14.5 1
70.5 1
..
18.0 26
22.0 27
24.0 30
Name: count, Length: 88, dtype: int64
如果全部打印,结果实在太多,因为各种年龄的都有,这个时候也可以指定一些区间,例如0~10岁属于少儿组,10~20岁属于青年组,这就相当于将连续值进行了离散化:
df['Age'].value_counts(ascending=True,bins=5)#指定划分几个组
(64.084, 80.0] 11
(48.168, 64.084] 69
(0.339, 16.336] 100
(32.252, 48.168] 188
(16.336, 32.252] 346
Name: count, dtype: int64
把所有数据按年龄平均分成5组,这样看起来就舒服多了。求符合每组情况的数据各有多少,这些都是在实际数据处理过程中常用的技巧。
在分箱操作中还可以使用cut()函数,功能更丰富一些。首先创建一个年龄数组,然后指定3个判断 值,接下来就用这3个值把数据分组,也就是(10,40],(40,80]这两组,返回的结果分别表示当前年龄属于哪组。
ages=[15,18,20,21,22,34,41,52,63,79]
bins=[10,40,80]
bins_res=pd.cut(ages,bins)
bins_res
[(10, 40], (10, 40], (10, 40], (10, 40], (10, 40], (10, 40], (40, 80], (40, 80], (40, 80], (40, 80]]
Categories (2, interval[int64, right]): [(10, 40] < (40, 80]]
也可以打印其默认标签值:
pd.value_counts(bins_res)#各组总共人数
(10, 40] 6
(40, 80] 4
Name: count, dtype: int64
pd.cut(ages,[10,30,50,80]) #分成年轻人、中年人、老年人3组
[(10, 30], (10, 30], (10, 30], (10, 30], (10, 30], (30, 50], (30, 50], (50, 80], (50, 80], (50, 80]]
Categories (3, interval[int64, right]): [(10, 30] < (30, 50] < (50, 80]]
group_names=['Youth','Mille','Old']
pd.value_counts(pd.cut(ages,[10,20,50,80],labels=group_names))
Mille 4
Youth 3
Old 3
Name: count, dtype: int64
pivot数据透视表
先来创建一份比较有意思的数据
example = pd.DataFrame({'Month': ["January", "January", "January", "January",
"February", "February", "February", "February",
"March", "March", "March", "March"],
'Category': ["Transportation", "Grocery", "Household", "Entertainment",
"Transportation", "Grocery", "Household", "Entertainment",
"Transportation", "Grocery", "Household", "Entertainment"],
'Amount': [74., 235., 175., 100., 115., 240., 225., 125., 90., 260., 200., 120.]})
其中Category表示把钱花在什么用途上(如交通运输、家庭、娱乐等费用),Month表示统计月份, Amount表示实际的花费。
下面要统计的就是每个月花费在各项用途上的金额分别是多少:
example_pivot = example.pivot(index = 'Category',columns= 'Month',values = 'Amount')
example_pivot
index指定了按照什么属性来统计,columns指定了统计哪个指标,values指定了统计的实际指标值是什么。
这几个月中每项花费的总额:
example_pivot.sum(axis=1)
Category
Entertainment 345.0
Grocery 735.0
Household 600.0
Transportation 279.0
dtype: float64
每个月所有花费的总额:
example_pivot.sum(axis=0)
Month
February 705.0
January 584.0
March 670.0
dtype: float64
df.pivot_table(index='Sex',columns='Pclass',values='Fare',aggfunc='max')#各个船舱的最大票价
这里得到的结果就是各个船舱的最大票价,需要额外指定aggfunc来明确结果的含义。
统计各个船舱等级的人数
df.pivot_table(index='Sex',columns='Pclass',values='Fare',aggfunc='count')#统计各个船舱等级的人数
接下来做一个稍微复杂点的操作,首先按照年龄将乘客分成两组:成年人和未成年人。再对这两组乘客分别统计不同性别的人的平均获救可能性:
df['Underaged']=df['Age']<=18
df.pivot_table(index='Underaged',columns='Sex',values='Survived',aggfunc='mean')
groupby操作
df = pd.DataFrame({'key':['A','B','C','A','B','C','A','B','C'],
'data':[0,5,10,5,10,15,10,15,20]})
df
key data
0 A 0
1 B 5
2 C 10
... ... ...
6 A 10
7 B 15
8 C 20
此时如果想统计各个key中对应的data数值总和是多少,例如key为A时对应3条数据:0、5、10,总和 就是15。按照正常的想法,需要把key中所有可能结果都遍历一遍,并且还要求各个key中的数据累加值:
for key in ['A','B','C']:
print(key,df[df['key']==key].sum())
A key AAA
data 15
dtype: object
B key BBB
data 30
dtype: object
C key CCC
data 45
dtype: object
更简单的方法
df.groupby('key').sum()
data
key
A 15
B 30
C 45
统计的结果是其累加值,当然,也可以换成均值等指标
df.groupby('key').aggregate(np.mean)
data
key
A 5.0
B 10.0
C 15.0
继续回到泰坦尼克号数据集中,下面要计算的是按照不同性别统计其年龄的平均值,所以要用 groupby计算一下性别:
df.groupby('Sex')['Age'].mean()
Sex
female 27.915709
male 30.726645
Name: Age, dtype: float64
结果显示乘客中所有女性的平均年龄是27.91,男性平均年龄是30.72,只需一行就完成了统计工作。
groupby()函数中还有很多参数可以设置,再深入了解一下:
df=pd.DataFrame({'A':['foo','bar','foo','bar','foo','bar','foo','foo'],
'B':['one','two','one','three','two','two','one','three'],
'C':np.random.randn(8),
'D':np.random.randn(8)})
df
此时想观察groupby某一列后结果的数量,可以直接调用count()属性:
grouped=df.groupby('A') #当A在取不同key值时,B、C、D中样本的数量
grouped.count()
B C D
A
bar 3 3 3
foo 5 5 5
结果中3和5分别对应了原始数据中样本的个数,可以亲自来数一数。这里不仅可以指定一个groupby对象,指定多个也是没问题的:
grouped=df.groupby(['A','B'])
grouped.count()
C D
A B
bar three 1 1
two 2 2
foo one 3 3
three 1 1
two 1 1
指定好操作对象之后,通常还需要设置一下计算或者统计的方法,比如求和操作:
grouped=df.groupby(['A','B'])
grouped.aggregate(np.sum)
C D
A B
bar three 0.055676 -1.690617
two -0.081661 -1.688381
foo one -0.351590 1.395628
three 0.816454 -0.668172
two 0.222400 -1.356399
此处的索引就是按照传入参数的顺序来指定的,如果大家习惯用数值编号索引也是可以的,只需要 加入as_index参数
groupby操作之后仍然可以使用describe()方法来展示所有统计信息,这里只展示前5条:
grouped.describe().head()
看起来统计信息有点多,当然也可以自己设置需要的统计指标:
grouped=df.groupby('A')
grouped['C'].agg([np.sum,np.mean,np.std])
常用函数操作
Merge操作
数据处理中可能经常要对提取的特征进行整合,例如我们拿到一份歌曲数据集,但是不同的文件存储的特征不同,有的文件包括歌曲名、播放量;有的包括歌曲名、歌手名。现在我们要做的就 是把所有特征汇总在一起,例如以歌曲为索引来整合。
为了演示Merge函数的操作,先创建两个DataFrame:
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']})
res=pd.merge(left,right,on='key')
key A B C D
0 K0 A0 B0 C0 D0
1 K1 A1 B1 C1 D1
2 K2 A2 B2 C2 D2
3 K3 A3 B3 C3 D3
现在按照key列把两份数据整合在一起了,key列在left和right两份数据中恰好都一样,试想:如果不相同,结果会发生变化吗?
left = pd.DataFrame({'key1':['K0','K1','K2','K3'],
'key2':['K0','K1','K2','K3'],
'A':['A0','A1','A2','A3'],
'B':['B0','B1','B2','B3']})
right = pd.DataFrame({'key1':['K0','K1','K2','K3'],
'key2':['K0','K1','K2','K4'],
'C':['C0','C1','C2','C3'],
'D':['D0','D1','D2','D3']})
pd.merge(left,right,on=['key1','key2'])
key1 key2 A B C D
0 K0 K0 A0 B0 C0 D0
1 K1 K1 A1 B1 C1 D1
2 K2 K2 A2 B2 C2 D2
输出结果显示前3行相同的都组合在一起了,但是第4行却被直接抛弃了。如果想考虑所有的结果, 还需要额外设置一个how参数:
pd.merge(left,right,on=['key1','key2'],how='outer')
key1 key2 A B C D
0 K0 K0 A0 B0 C0 D0
1 K1 K1 A1 B1 C1 D1
2 K2 K2 A2 B2 C2 D2
3 K3 K3 A3 B3 NaN NaN
4 K3 K4 NaN NaN C3 D3
还可以加入详细的组合说明,指定indicator参数为True即可:
pd.merge(left,right,on=['key1','key2'],how='outer',indicator = True)
key1 key2 A B C D _merge
0 K0 K0 A0 B0 C0 D0 both
1 K1 K1 A1 B1 C1 D1 both
2 K2 K2 A2 B2 C2 D2 both
3 K3 K3 A3 B3 NaN NaN left_only
4 K3 K4 NaN NaN C3 D3 right_only
也可以单独设置只考虑左边数据或者只考虑右边数据,说白了就是以谁为准:
pd.merge(left,right,on=['key1','key2'],how='left')
key1 key2 A B C D
0 K0 K0 A0 B0 C0 D0
1 K1 K1 A1 B1 C1 D1
2 K2 K2 A2 B2 C2 D2
3 K3 K3 A3 B3 NaN NaN
排序操作
排序操作的用法也是十分简洁,先来创建一个DataFrame:
data=pd.DataFrame({'group':['a','a','a','b','b','b','c','c','c'],
'data':[4,3,2,1,12,3,4,5,6]})
排序的时候,可以指定升序或者降序,并且还可以指定按照多个指标排序:
data.sort_values(by=['group','data'],ascending=[False,True],inplace=True)
data
group data
6 c 4
7 c 5
8 c 6
... ... ...
2 a 2
1 a 3
0 a 4
上述操作表示首先对group列按照降序进行排列,在此基础上保持data列是升序排列,其中by参数用于设置要排序的列,ascending参数用于设置升降序。
缺失值处理
拿到一份数据之后,经常会遇到数据不干净的现象,即里面可能存在缺失值或者重复片段,这就需 要先进行预处理操作。再来创建一组数据,如果有重复部分,也可以直接用乘法来创建一组数据:
data=pd.DataFrame({'k':['one']*3+['two']*4,
'k2':[3,2,1,3,3,4,4]})
data
k k2
0 one 3
1 one 2
2 one 1
... ... ...
4 two 3
5 two 4
6 two 4
此时数据中有几条完全相同的,可以使用drop_duplicates()函数去掉多余的数据:
data.drop_duplicates() # 删去重复值
k k2
0 one 3
1 one 2
2 one 1
3 two 3
5 two 4
也可以只考虑某一列的重复情况,其他全部舍弃:
data.drop_duplicates(subset='k')
k k2
0 one 3
3 two 3
如果要往数据中添加新的列呢?可以直接指定新的列名或者使用assign()函数:
df=pd.DataFrame({'data1':np.random.randn(5),
'data2':np.random.randn(5)})
df2=df.assign(ration=df['data1']/df['data2'])
df2
data1 data2 ration
0 0.237845 0.736205 0.323068
1 0.013549 1.026921 0.013193
2 -1.635529 -1.432191 1.141977
3 -1.044210 -1.841188 0.567139
4 0.613039 0.366093 1.674543
数据处理过程中经常会遇到缺失值,Pandas中一般用NaN来表示(Not a Number),拿到数据之后, 通常都会先看一看缺失情况:
df=pd.DataFrame([range(3),[0,np.nan,0],[0,0,np.nan],range(3)])
df
在创建的时候加入两个缺失值,可以直接通过isnull()函数判断所有缺失情况:
df.isnull()
0 1 2
0 False False False
1 False True False
2 False False True
3 False False False
输出结果显示了全部数据缺失情况,其中True代表数据缺失。如果数据量较大,总不能一行一行来 核对,更多的时候,我们想知道某列是否存在缺失值:
df.isnull().any()
0 False
1 True
2 True
dtype: bool
其中.any()函数相当于只要有一个缺失值就意味着存在缺失情况,当然也可以自己指定检查的维度:
df.isnull().any(axis=1)
0 False
1 True
2 True
3 False
dtype: bool
遇到缺失值不要紧,可以选择填充方法来改善,之后会处理实际数据集的缺失问题,这里只做简单 举例:
df.fillna(5)#对缺失值进行填充
0 1 2
0 0 1.0 2.0
1 0 5.0 0.0
2 0 0.0 5.0
3 0 1.0 2.0
通过fillna()函数可以对缺失值进行填充,这里只选择一个数值,实际中更常使用的是均值、中位数等指标,还需要根据具体问题具体分析。
apply自定义函数
apply()函数可是一个“神器”,如果你想要完成的任务没办法直接实现, 就需要使用apply自定义函数功能,还是先来看看其用法:
data=pd.DataFrame({'food':['A1','A2','B1','B2','B3','C1','C2'],
'data':[1,2,3,4,5,6,7]})
data
food data
0 A1 1
1 A2 2
2 B1 3
... ... ...
4 B3 5
5 C1 6
6 C2 7
def food_map(series):
if series['food']=='A1':
return 'A'
elif series['food']=='A2':
return 'A'
elif series['food']=='B1':
return 'B'
elif series['food']=='B2':
return 'B'
elif series['food']=='B3':
return 'B'
elif series['food']=='C1':
return 'C'
elif series['food']=='C2':
return 'C'
data['food_map']=data.apply(food_map,axis='columns')
data
food data food_map
0 A1 1 A
1 A2 2 A
2 B1 3 B
... ... ... ...
4 B3 5 B
5 C1 6 C
6 C2 7 C
上述操作首先定义了一个映射函数,如果想要改变food列中的所有值,在已经给出映射方法的情况 下,如何在数据中执行这个函数,以便改变所有数据呢?是不是要写一个循环来遍历每一条数据呢?肯定不是的,只需调用apply()函数即可完成全部操作。
可以看到,apply()函数使用起来非常简单,需要先写好要执行操作的函数,接下来直接调用即可, 相当于对数据中所有样本都执行这样的操作,下面继续拿泰坦尼克号数据来试试apply()函数:
def nan_count(columns):
columns_null=pd.isnull(columns)
null=columns[columns_null]
return len(null)
columns_null_count=titanic.apply(nan_count)
这里要统计的就是每列的缺失值个数,写好自定义函数之后依旧调用apply()函数,这样每列特征的 缺失值个数就统计出来了,再来统计一下每一位乘客是否是成年人:
def is_mirror(row):
if row['Age']<18:
return True
else:
return False
mirrors=titanic.apply(is_minor,axis=1)
时间操作
当拿到一份时间特征时,最好还是将其转换成标准格式,这样在提取特征时更方便一些:
ts=pd.Timestamp('2017-11-24')
ts
Timestamp('2017-11-24 00:00:00')
ts.month
11
ts.day
24
ts+pd.Timedelta('5 days')
Timestamp('2017-11-29 00:00:00')
时间特征只需要满足标准格式就可以调用各种函数和属性了,上述操作通过时间提取了当前具体的 年、月、日等指标。
s = pd.Series(['2017-11-24 00:00:00','2017-11-25 00:00:00','2017-11-26 00:00:00'])
s
0 2017-11-24 00:00:00
1 2017-11-25 00:00:00
2 2017-11-26 00:00:00
dtype: object
ts = pd.to_datetime(s)
ts
0 2017-11-24
1 2017-11-25
2 2017-11-26
dtype: datetime64[ns]
一旦转换成标准格式,注意其dtype类型,就可以调用各种属性进行统计分析了:
ts.dt.hour
0 0
1 0
2 0
dtype: int32
ts.dt.weekday
0 4
1 5
2 6
dtype: int32
如果数据中没有给定具体的时间特征,也可以自己来创建,例如知道数据的采集时间,并且每条数 据都是固定时间间隔保存下来的:
pd.Series(pd.date_range(start='2017-11-24',periods = 10,freq = '12H'))
0 2017-11-24 00:00:00
1 2017-11-24 12:00:00
2 2017-11-25 00:00:00
...
7 2017-11-27 12:00:00
8 2017-11-28 00:00:00
9 2017-11-28 12:00:00
Length: 10, dtype: datetime64[ns]
读取数据时,如果想以时间特征为索引,可以将parse_dates参数设置为True:
data=pd.read_csv('./data/flowdata.csv',index_col=0,parse_dates=True)
data
L06_347 LS06_347 LS06_348
Time
2009-01-01 00:00:00 0.137417 0.097500 0.016833
2009-01-01 03:00:00 0.131250 0.088833 0.016417
2009-01-01 06:00:00 0.113500 0.091250 0.016750
... ... ... ...
2013-01-01 18:00:00 1.178583 1.178583 0.083083
2013-01-01 21:00:00 0.898250 0.898250 0.077167
2013-01-02 00:00:00 0.860000 0.860000 0.075000
有了索引后,就可以用它来取数据:
data[pd.Timestamp('2012-01-01 09:00'):pd.Timestamp('2012-01-01 19:00')]
L06_347 LS06_347 LS06_348
Time
2012-01-01 09:00:00 0.330750 0.293583 0.029750
2012-01-01 12:00:00 0.295000 0.285167 0.031750
2012-01-01 15:00:00 0.301417 0.287750 0.031417
2012-01-01 18:00:00 0.322083 0.304167 0.038083
也用data['2012-01':'2012-03']指定具体月份,或者更细致一些,在小时上继续进行判断,如 data[(data.index.hour>8)&(data.index.hour<12)]。
data[data.index.year==2013]
L06_347 LS06_347 LS06_348
Time
2013-01-01 00:00:00 1.688333 1.688333 0.207333
2013-01-01 03:00:00 2.693333 2.693333 0.201500
2013-01-01 06:00:00 2.220833 2.220833 0.166917
... ... ... ...
2013-01-01 18:00:00 1.178583 1.178583 0.083083
2013-01-01 21:00:00 0.898250 0.898250 0.077167
2013-01-02 00:00:00 0.860000 0.860000 0.075000
下面再介绍一个重量级的家伙,在处理时间特征时候经常会用到它——resample重采样,先来看看执行结果:
data.resample('D').mean().head()
L06_347 LS06_347 LS06_348
Time
2009-01-01 0.125010 0.092281 0.016635
2009-01-02 0.124146 0.095781 0.016406
2009-01-03 0.113562 0.085542 0.016094
2009-01-04 0.140198 0.102708 0.017323
2009-01-05 0.128812 0.104490 0.018167
原始数据中每天都有好几条数据,但是这里想统计的是每天的平均指标,当然也可以计算其最大 值、最小值,只需把.mean()换成.max()或者.min()即可。
例如想按3天为一个周期进行统计:
data.resample('3D').mean().head()#按3天为一个周期进行统计
L06_347 LS06_347 LS06_348
Time
2009-01-01 0.120906 0.091201 0.016378
2009-01-04 0.121594 0.091708 0.016670
2009-01-07 0.097042 0.070740 0.014479
2009-01-10 0.115941 0.086340 0.014545
2009-01-13 0.346962 0.364549 0.034198
按月进行统计也是同理:
data.resample('ME').mean().head()#按月进行统计
L06_347 LS06_347 LS06_348
Time
2009-01-31 0.517864 0.536660 0.045597
2009-02-28 0.516847 0.529987 0.047238
2009-03-31 0.373157 0.383172 0.037508
2009-04-30 0.163182 0.129354 0.021356
2009-05-31 0.178588 0.160616 0.020744
时间数据可以提取出非常丰富的特征,不仅有年、月、日等常规指标,还可以判断是否是周末、工作日、上下旬、上下班时间、节假日等特征,这些特征对数据挖掘任务都是十分有帮助的。
绘图操作
%matplotlib inline
df=pd.DataFrame(np.random.randn(10,4).cumsum(0),index=np.arange(0,100,10),
columns=['A','B','C','D'])
df.plot()
虽然直接对数据执行plot()操作就可以完成基本绘制,但是,如果想要加入一些细节,就需要使用 Matplotlib工具包,例如要同时展示两个图表,就要用到子图:
import matplotlib.pyplot as plt
fig,axes=plt.subplots(2,1)
data=pd.Series(np.random.rand(16),index=list('abcdefghigklmnop'))
data.plot(ax=axes[0],kind='bar')
data.plot(ax=axes[1],kind='barh')
还可以指定绘图的种类,例如条形图、散点图等:
df = pd.DataFrame(np.random.rand(6, 4),
index = ['one', 'two', 'three', 'four', 'five', 'six'],
columns = pd.Index(['A', 'B', 'C', 'D'], name = 'Genus'))
df.head()
df.plot(kind='bar')
macro=pd.read_csv('./data/macrodata.csv')
data = macro[['quarter','realgdp','realcons']]
data.plot.scatter('quarter','realgdp')
大数据处理技巧
使用Pandas工具包可以处理千万级别的数据量,但读取过于庞大的数据特征时,经常会遇到内存溢出 等问题。估计绝大多数读者使用的笔记本电脑都是8GB内存,没关系,这里教给大家一些大数据处理技巧,使其能够占用更少内存。
数值类型转换
下面读取一个稍大数据集,特征比较多,一共有161列,目标就是尽可能减少占用的内存。
g1=pd.read_csv('./data/game_logs.csv')
g1.head()
g1.shape
(171907, 161)
g1.info(memory_usage='deep')
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 171907 entries, 0 to 171906
Columns: 161 entries, date to acquisition_info
dtypes: float64(77), int64(6), object(78)
memory usage: 859.4 MB
输出结果显示这份数据读取进来后占用860.5MB内存,数据类型主要有3种,其中,float64类型有77个特征,int64类型有6个特征,object类型有78个特征。
对于不同的数据类型来说,其占用的内存相同吗?应该是不同的,先来计算一下各种类型平均占用 内存:
for dtype in ['float64','object','int64']:
selected_dtype = g1.select_dtypes(include=[dtype])
mean_usage_b = selected_dtype.memory_usage(deep=True).mean()
mean_usage_mb = mean_usage_b / 1024 ** 2
print("Average memory usage for {} columns: {:03.2f} MB".format(dtype,mean_usage_mb))
循环中会遍历3种类型,通过select_dtypes()函数选中属于当前类型的特征,接下来计算其平均占用内存,最后转换成MB看起来更直接一些。从结果可以发现,float64类型和int64类型平均占用内存差不多, 而object类型占用的内存最多。
接下来就要分类型对数据进行处理,首先处理一下数值型,经常会看到有int64、int32等不同的类型,它们分别表示什么含义呢?
int_types = [ "int8", "int16","int32","int64"]
for it in int_types:
print(np.iinfo(it))
Machine parameters for int8
---------------------------------------------------------------
min = -128
max = 127
---------------------------------------------------------------
Machine parameters for int16
---------------------------------------------------------------
min = -32768
max = 32767
---------------------------------------------------------------
Machine parameters for int32
---------------------------------------------------------------
min = -2147483648
max = 2147483647
---------------------------------------------------------------
Machine parameters for int64
---------------------------------------------------------------
min = -9223372036854775808
max = 9223372036854775807
---------------------------------------------------------------
输出结果分别打印了int8~int64可以表示的数值取值范围,int8和int16能表示的数值范围有点儿小, 一般不用。int32看起来范围足够大了,基本任务都能满足,而int64能表示的就更多了。原始数据是int64 类型,但是观察数据集可以发现,并不需要这么大的数值范围,用int32类型就足够了。下面先将数据集中所有int64类型转换成int32类型,再来看看内存占用会不会减少一些。
def mem_usage(pandas_obj):
if isinstance(pandas_obj,pd.DataFrame):
usage_b = pandas_obj.memory_usage(deep=True).sum()
else:
usage_b = pandas_obj.memory_usage(deep=True)
usage_mb = usage_b / 1024 ** 2
return "{:03.2f} MB".format(usage_mb)
gl_int = g1.select_dtypes(include=['int64'])
converted_int = gl_int.apply(pd.to_numeric,downcast='integer')
print(mem_usage(gl_int))
print(mem_usage(converted_int))
7.87 MB
1.80 MB
其中mem_usage()函数的主要功能就是计算传入数据的内存占用量,为了让程序更通用,写了一个判断方法,分别表示计算DataFrame和Series类型数据,如果包含多列就求其总和,如果只有一列,那就是它自身。select_dtypes(include=['int64'])表示此时要处理的是全部int64格式数据,先把它们都拿到手。接下来对这部分数据进行向下转换,可以通过打印coverted_int.info()来观察转换结果。
可以看到在进行向下转换的时候,程序已经自动地选择了合适类型,再来看看内存占用情况,原始 数据占用7.87MB,转换后仅占用1.80MB,大幅减少了。由于int型数据特征并不多,差异还不算太大,转换float类型的时候就能明显地看出差异了。
gl_float = g1.select_dtypes(include=['float64'])
converted_float = gl_float.apply(pd.to_numeric,downcast='float')
print(mem_usage(gl_float))
print(mem_usage(converted_float))
100.99 MB
50.49 MB
可以明显地发现内存节约了正好一半,通常在数据集中float类型多一些,如果对其进行合适的向下转换,基本上能节省一半内存。
属性类型转换
最开始就发现object类型占用内存最多,也就是字符串,可以先看看各列object类型的特征:
gl_obj = g1.select_dtypes(include=['object']).copy()
gl_obj.describe()
其中count表示数据中每一列特征的样本个数(有些存在缺失值),unique表示不同属性值的个数, 例如day_of_week列表示当前数据是星期几,所以只有7个不同的值,但是默认object类型会把出现的每一条样本数值都开辟一块内存区域。
很明显,星期一和星期二出现多次,它们只是一个字符串代表一种结果而已,共用一块内存就足够了。但是在object类型中却为每一条数据开辟了单独的一块内存,一共有171907条数据,但只有7个不同值,这样做浪费,所以还是要把object类型转换成category类型。
dow = gl_obj.day_of_week
dow_cat = dow.astype('category')
print(dow_cat.head())
0 Thu
1 Fri
2 Sat
3 Mon
4 Tue
Name: day_of_week, dtype: category
Categories (7, object): ['Fri', 'Mon', 'Sat', 'Sun', 'Thu', 'Tue', 'Wed']
可以发现,其中只有7种编码方式,也可以实际打印一下具体编码:
dow_cat.head(10).cat.codes
无论打印多少条数据,其编码结果都不会超过7种,这就是category类型的特性,相同的字符占用一块内存就好了。转换完成之后,是时候看看结果了:
print(mem_usage(dow))
print(mem_usage(dow_cat))
9.84 MB
0.16 MB
对day_of_week列特征进行转换后,内存占用大幅下降,效果十分明显,其他列也是同理,但是,如果不同属性值比较多,效果也会有所折扣。接下来对所有object类型都执行此操作:
converted_obj = pd.DataFrame()
for col in gl_obj.columns:
num_unique_values = len(gl_obj[col].unique())
num_total_values = len(gl_obj[col])
if num_unique_values / num_total_values < 0.5:
converted_obj.loc[:,col] = gl_obj[col].astype('category')
else:
converted_obj.loc[:,col] = gl_obj[col]
print(mem_usage(gl_obj))
print(mem_usage(converted_obj))
750.57 MB
49.89 MB
首先对object类型数据中唯一值个数进行判断,如果数量不足整体的一半(此时能共用的内存较 多),就执行转换操作,如果唯一值过多,就没有必要执行此操作。最终的结果非常不错,内存只占用很小部分了。 这个例子中,内存从859.4MB下降到49.89MB,效果还是十分明显的。
如果加载千万级别以上数据源,还是有必要对数据先进行上述处理,否则会经常遇到内存溢出错误。