如何溜溜地使用pandas操作数据(一)
注:本文版权由Alfred所有,首发于微信公众号「DataCastle数据城堡」(ID:DataCastle2016),如需转载还请提前联络songdi@datacastle.cn,非常感谢。
Alfred,一个搞数据挖矿的,煎的一手好牛排。曾撰文「一件有趣的事,爬了爬自己的微信朋友圈」被多平台大大大大大大量转载。欢迎你搜狗一下~
如何操作pandas?
用一个实际的数据集练手可能是最快最好的方法。
今天就和大家一起探索pandas关于数据操作和处理的基础方法。
数据来源于DC竞赛的《智慧校园全球大学生创意应用大赛》其中的一个csv文件数据,点击报名参赛便可以获得数据下载链接。本次使用的数据是SiChuanSchool文件夹中的“电子科技大学用户统计-表格 1.csv”文件。
注意
1.运行环境是Python3;
2.由于运行过程中可能有一些结果被我重新编辑或者删去了,所以不要太在意In[ ]的编号顺序;
3.更多更加全面更加正规的使用方法可以阅读pandas的官方文档和《利用Python进行数据分析》(这本书有些方法已经过时了,学习的时候要注意转换);
4.另外,在数据处理的过程中,每一步处理之前先保存好之前的数据是一个良好的习惯,可以免去由于某一步操作错误又要重新处理数据的麻烦。
目录¶
1.导入数据
2.数据初探
3.行/列选取
4.更多行列操作
5.行列删除
6.数据类型的转换
7.数据过滤
8.数据排序
9.数据的描述统计
10.处理缺失的数据
11.数据保存
1.导入数据¶
In [44]:
#首先import pandas
import pandas as pd
In [45]:
#把位于桌面的数据读取进来,由于我用的是Mac,路径可能跟Windows不同
data = pd.read_csv('/Users/apple/Desktop/电子科技大学用户统计-表格 1.csv')
2.数据初探¶
In [46]:
#首先看看数据长什么样子,head()默认获得表格前6行的数据
#也可以在括号里输入想要返回的行数,如data.head(10)返回前10行的数据
#另外有一个需要注意的地方就是,数据导入的时候如果没有特别的参数设置,pandas会自动为数据添加索引,如下面数据框中最左边的0-4就是自动添加的索引
data.head()
Out[46]:
In [47]:
#使用tail()返回末尾的数据
data.tail(3)
Out[47]:
In [48]:
#有时候由于数据太多,我们不能确定知道了数据的首尾就可以掌握数据的大致情况,
#可以使用sample()来随机抽取出数据中的指定条数,如下随机抽取了5条数据
data.sample(5)
Out[48]:
In [49]:
#查看数据有多少行多少列,注意shape后面不带括号
data.shape
#可以看到数据有54925行,11列
Out[49]:
In [50]:
#查看数据的列名
data.columns
Out[50]:
In [51]:
#使用describe()返回数据描述性统计的值:count(计数),mean(平均值),std(标准差),min(最小值),25%(Q1四分位数),50%(Q2四分位数,即中位数),75%(Q3四分位数),max(最大值)
data.describe()
Out[51]:
In [52]:
#奇怪了,明明“发博量”和“粉丝量”字段都是数值型数据啊,为什么只返回了“UID”的统计?
#使用info看一下各个字段被读取进来的时候的数据类型
data.info()
#可以看到数据读取进来的时候,“UID”被识别成了int型,其他都被默认识别成object
#这就涉及到数据类型的转换,后面会讲到,先跳过(先来学习一些基础知识)
3.行/列选取¶
In [53]:
#如果想要获得指定位置的某条记录,比如索引号为666的记录,可以使用:
data.loc[666]
#其实就是行选取
Out[53]:
In [54]:
#想要获取666-668的记录,使用:
data.loc[666:668]
#注意和data[666:668]的区别(自行实现对比一下)
Out[54]:
In [55]:
#想要获取666,777,888的记录,使用:
data.loc[[666,777,888]]
Out[55]:
In [56]:
#想要获取特定列(字段)的记录,比如“昵称”列,:
data['昵称']
Out[56]:
In [57]:
#获取多列的记录,传入一个列表:
data[['昵称','认证信息']]
Out[57]:
In [58]:
#获取字段“昵称”,“认证信息”的前11行的记录
data.loc[:10,['昵称','认证信息']]
#注意观察代码,逗号前面指定行,逗号后面指定列
Out[58]:
In [59]:
#获取前5行,第9-11列的数据
data.iloc[:5,8:]
#注意loc和iloc的区别
Out[59]:
4.更多行列操作¶
In [60]:
#在数据表中新增一列“test”,赋值为NaN(缺失值)
#首先把numpy导入进来,因为要用到numpy的缺失值nan
import numpy as np
data['test'] = np.nan
#好了,看一下现在的数据长什么样子,可以看到最右边已成功添加一列“test”,值为NaN
data.head()
Out[60]:
In [61]:
#把“test”列重新赋值为999
data['test'] = 999
#瞅一眼数据
data.head()
Out[61]:
In [62]:
#把“test”列按照规则赋值:每条记录的“v区分”字段是否是“橙V”
data['test'] = data.v区分 == '橙V'
#瞅一眼数据。data.v区分 == '橙V'返回的是一串boolean值True/False,如果是“橙V”则为True,否则是False
data.head()
Out[62]:
In [63]:
#于是结合上面的方法,可以这样来赋值:
#如果是'橙V'便赋值为'这丫竟是橙V,大家快来孤立他!',如果不是'橙V'便赋值为'辛亏不是橙V'
data.loc[data.v区分 == '橙V','test'] = '这丫竟是橙V,大家快来孤立他!'
data.loc[data.v区分 != '橙V','test'] = '辛亏不是橙V'
#再瞅一眼数据
data.head()
Out[63]:
In [64]:
#于是,由上面的方法可以引出,如果要想给数据特定的位置赋值,比如想要把‘test’列的2-4行赋值为“我不听我不听!”,可以这样:
data.loc[2:4,'test'] = '我不听我不听!'
data.head()
Out[64]:
In [65]:
#如果想要更改某列的列名,怎么做呢?
data.rename(columns={'test':'乱加的一列'}, inplace = True)
#使用rename,指定columns,使用字典mapping(映射)的方法改变,记得加上inplace=True,表示直接在数据表data上固化这个操作
data.head()
Out[65]:
In [66]:
#如果想要更改所有的列名,怎么做呢?
#为了不直接在data数据集上更改,新起一个只包含data数据表中前6行的数据表data_test
data_test=data[:6]
#使用直接传入一个列表的方式更改,这里把它改为['列1','列2','列3','列4','列5','列6','列7','列8','列9','列10','列11','列12']
#这种方法要求列表中的列名顺序必须与原数据一致,还有一种方法是使用上面的字典mapping(映射),大家可以思考一下怎么操作。
data_test.columns=['列1','列2','列3','列4','列5','列6','列7','列8','列9','列10','列11','列12']
data_test
Out[66]:
In [67]:
#列按照列名重新排列:比如说,你觉得现在每列的排列你看起来很不爽,想要把列4排在列3的前面,列8排在列7的前面,列12排在列4的后面,怎么做?
#使用reindex,指定是针对columns
data_test.reindex(columns=['列1','列2','列4','列12','列3','列5','列6','列8','列7','列9','列10','列11'])
Out[67]:
In [68]:
#行按照索引重新排列,同样是使用reindex,如果不明确指定是针对index还是columns,默认操作是针对index
#当然也可以使用data_test.reindex(index = [3,4,5,0,1,2])明确指定,让代码更具有可读性
data_test.reindex([3,4,5,0,1,2])
Out[68]:
5.行列删除¶
In [69]:
#删除特定的行,比如说想要删除第2,第5行
data_test.drop([2,5], axis=0)
#注意,如果想要固化删除这个操作,需要使用data_test = data_test.drop([2,5], axis=0)或者加上inplace=True
Out[69]:
In [70]:
#同样,想要删除特定列,可以使用:
data_test.drop(['列1','列2'], axis = 1)
#axis=0表示行所在的轴,axis=1表示列所在的轴
Out[70]:
6.数据类型的转换¶
In [71]:
#有了前面的基础知识,现在我们回到一开始的数据类型的问题。
#先看“发博量”字段,首先使用data['发博量']或者data['发博量'].sample(10)瞅一眼数据
data['发博量'].sample(10)
#发现数据之所以会在导入时被默认成object,是因为中间不只有数值型数据,还有“-”,代表缺失数据或者代表发博量为0
#到底“-”代表的是缺失数据或还是发博量为0呢?原数据没有给出太多的说明,这值得我们深究,以决定到底使用什么方法处理这个数据(直接丢弃/替换成NaN/替换成0/...)。
Out[71]:
In [72]:
#现先认为“-”代表的是发博量为0,那么需要把“-”替换成0
data.loc[data.发博量 == '-','发博量'] = 0
#瞅一眼数据,可以发现“-”已被成功替换成0
data['发博量'].sample(10)
Out[72]:
In [73]:
#接下来就可以进行数据类型的转换啦
data.发博量 = pd.to_numeric(data.发博量)
#查看数据类型,可以发现“发博量”的数据类型已成功被更改为int(64)
data.info()
In [75]:
#同样的方法处理一下“粉丝量”字段
#思考一下,如果不替换“-”,直接使用pd.to_numeric会出现什么状况?怎么处理?(自行操作一下,提示:去查找一下pd.to_numeric更加全面的用法,以及data.粉丝量 = pd.to_numeric(data.粉丝量, errors='coerce')这段代码的含义)
data.loc[data.粉丝量 == '-','粉丝量'] = 0
data.粉丝量 = pd.to_numeric(data.粉丝量)
#看一眼数据,发现“粉丝量”被处理成了float64,大家可以思考一下为什么,以及怎么处理?
data.info()
In [76]:
#同样需要处理的还有“最近发博时间”字段,把object型转换成datetime型
#首先看一眼数据的基本情况,可以发现数据存储的格式是月/日/年,但是还有一个异常数据,那就是“1900/1/0”,这个可以定为缺失数据
data.最近发博时间.sample(10)
Out[76]:
In [77]:
#直接使用pd.to_datetime转换,对于时间是“1900/1/0”的直接转换成NaT
data.最近发博时间 = pd.to_datetime(data['最近发博时间'], errors='coerce')
In [78]:
#相信你已经注意到上面的代码中我混用了“data.最近发博时间”和“data['最近发博时间']”,请自行去了解一下
#瞅一眼数据,可以发现数据已被成功转换成datetime64型,“1900/1/0”也被成功转换成NaT
data.最近发博时间.sample(10)
Out[78]:
7.数据过滤¶
In [80]:
#前面已经涉及了一些数据过滤的操作了,这里再深入一些探究
#假如我只想选取粉丝量大于1000000的微博记录(由于数据有75条,太多了不好显示,所以我加了head()):
data[data['粉丝量']>1000000].head()
Out[80]:
In [81]:
#选取粉丝量在1000000到1200000之间的数据
data[(data['粉丝量']>1000000) & (data['粉丝量']<1200000)]
#这里可以看到,有些微博发博量那么小竟然有那么多的粉丝,这让我开始质疑这里的发博量是怎么统计的,以及数据真实性,这个原因值得我们探寻。当然这是外话,暂且按下不表。
Out[81]:
In [82]:
#假如说我对于不是大V(也就是普通用户)同时粉丝量又大于100万的微博感兴趣,如何选取呢?
#到了这一步,我想要谈一谈数据过滤操作的原理:data['粉丝量']>1000000返回的是一串boolean值(True/False),大于100万的为True,小于或等于100万的为False,再把这串boolean值传入data[]进行数据选取,返回对应值为True的记录
#符号“&”是“且”的意思,表示只有两个条件同时满足才会返回True,否则返回False
#或且非:“|”是“或”的意思,“&”是“且”的意思,“!=”是“非”的意思,可以自行实现看看
data[(data['粉丝量']>1000000) & (data['v区分'] == '普通用户')]
Out[82]:
In [83]:
#再在前面的基础上添上一个条件:层级不是“C5”的
data[(data['粉丝量']>1000000) & (data['v区分'] == '普通用户') & (data['层级'] != 'C5')]
Out[83]:
8.数据排序¶
In [84]:
#观察数据发现,数据应该是按照发博量降序的,假如我想按照“粉丝量”排序,可以使用sort_values(),把想要排序的列传值给by:
#注意,这里为了更好显示还是使用了head()
data.sort_values(by='粉丝量').head()
Out[84]:
In [87]:
#哎呀妈呀,默认的是升序排列,我想要的是按照“粉丝量”降序排列,所以需要加上ascending=False
#选取数据表中粉丝量最多的前5个微博
data.sort_values(by='粉丝量',ascending=False).head()
Out[87]:
In [88]:
#假如我想要首先按照“最近发博时间”降序排序,“最近发博时间”相同的情况下再按照“粉丝量”降序排序,应该怎么做?
#此时只需要传给by一个列表就好啦
data.sort_values(by=['最近发博时间','粉丝量'],ascending=False).head()
Out[88]:
In [89]:
#另外需要注意的是在排序时,缺失值会被默认放在最后,看看这个按照“粉丝量”降序排序的数据末尾都是粉丝量为NaN的
data.sort_values(by='粉丝量',ascending=False).tail()
Out[89]:
9.数据的描述统计¶
In [92]:
#前面已经使用过describe()返回描述统计值,再看看sum(总和),mean(平均值),median(中值),std(标准差),count(计数),min(最小值),max(最大值),quantile(分位数)各自的操作
#以“粉丝量字段”为例,求粉丝量平均数,其他的同理
data['粉丝量'].mean()
Out[92]:
In [93]:
#有一点需要注意的地方就是,mean()会自动跳过缺失值(NaN),也就是说使用mean()计算出来的平均值是以“粉丝量”字段中非NaN值为基础的
#可以看看如果是以总体记录数为基础计算平均数,数值上的差别
data['粉丝量'].sum()/len(data['粉丝量'])
Out[93]:
In [94]:
#还有一个比较有用的操作是idxmin(返回最小值的索引),idxmax(返回最大值的索引)
#返回粉丝量最大的微博所在的索引,这里返回索引39
data['粉丝量'].idxmax()
Out[94]:
In [95]:
#可以根据索引号把最大值所在的记录调出来看看,原来是“中国日报”
data.loc[data['粉丝量'].idxmax()]
Out[95]:
In [96]:
#计算两列的相关系数
#假设:“发博量”与“粉丝量”是正相关的,使用corr计算相关系数
#可以看到返回的相关系数还是比较低的,其中的原因值得我们探讨
data.发博量.corr(data.粉丝量)
Out[96]:
In [97]:
#从data.head()中可以看到“层级”字段的值有C5,C2,C3等,我想知道“层级“字段都包含有哪些值,使用unique
#可以看到“层级”字段包含['C5', 'C2', 'C3', '无层级', 'C1', 'C4']等
data.层级.unique()
Out[97]:
In [98]:
#假如我想看看每个层级都有多少条记录,也就是计算每个层级的频数
data.层级.value_counts()
Out[98]:
10.处理缺失的数据¶
In [99]:
#前面已经穿插了一些缺失数据处理的内容,现在再深入一下来探讨
#为了简化说明,现在根据data这个数据集执行以下操作,构造一个简化的数据集data_test
data_test = data.sample(6)
data_test.loc[data_test['发博量']==0,'发博量']=np.nan
data_test.loc[44942]=np.nan
#瞅一眼这个构造好的数据集data_test
data_test
Out[99]:
In [100]:
#丢弃所有包含缺失数据的行(只要一行中包含一个缺失数据,即丢弃):
data_test.dropna()
Out[100]:
In [101]:
#dropna()默认丢弃所有包含缺失数据的行,如果我只想要丢弃44942行(一行数据的所有字段值都是NaN),其他都保留,需要使用一个参数how,把“all”传值给how,表示只有全是NaN值的记录才会被丢弃
#这里使用inplace=True固化删除44942行这个操作,因为下面的操作需要使用到这个结果
data_test.dropna(how='all',inplace=True)
#瞅一眼数据,可以发现只有原来的44942行被丢弃了
data_test
Out[101]:
In [102]:
#丢弃包含缺失数据的列(这个方法不常用到,了解一下就好)
data_test.dropna(axis = 1)
#可以发现包含NaN的“发博量”一列被删除了
Out[102]:
In [104]:
#如果决定处理缺失数据的方式不是直接丢弃,而是填充数据
#为了更好地说明操作方法,再构造一下数据集
data_test.loc[44942]=np.nan
data_test.loc[[49334,14818],'层级'] = np.nan
#瞅一眼数据长什么样子
data_test
Out[104]:
In [105]:
#好,接下来就可以进行填充缺失数据的操作了:填充数据使用的是fillna()方法,括号里传入填充的方法
#假如我想要把所有的缺失数据都填充为0,则在括号里直接传入0:
#可以看到所有的缺失数据都被成功填充为0(可能这里你就要怼我了:明明最后一行的最近发博时间没有被填充为0啊,其实,计算机默认的时间起点就是“1970-01-01”)
data_test.fillna(0)
Out[105]:
In [106]:
#假如我想要给每一列的缺失数据填充不一样的值,比如说给“层级”列填充“C0”,给“发博量”填充“999”,则传入一个字典mapping(映射)
data_test.fillna({'层级':'C0','发博量':'999'})
Out[106]:
In [108]:
#给发博量的缺失数据直接赋值为“999”真是太草率了,如果我想按照发博量的平均值填充缺失数据,则:
data_test['发博量'].fillna(data['发博量'].mean())
#因为data_test数据集的发博量都是1,所以我使用了原数据集data的发博量平均值来填充了
Out[108]:
In [109]:
#还有一种方法是向前填充(forward fill),这种填充方法很经常用在时间序列的缺失值填充里,这里只是利用这个数据集举个例
#向前填充的意思就是每行的实值往前(下)填充缺失值,比如说原来的数据中46397行的“发博量”为NaN值,就被上一行(51906行)的值向前填充了,如果下一行的“发博量”还是缺失值,还会继续往前填充
#由于49334行的“发博量”上面没有数据可以实现向前填充,所以使用ffill之后它的值还是NaN
#类似的方法还有向后填充(back fill),可以自行操作代码data_test.fillna(method='bfill')看看
data_test.fillna(method='ffill')
Out[109]:
In [110]:
#这种填充方法有一个bug,就是万一数据连着10行都是缺失数据的话,那么使用ffill的方法填充的话这10行缺失数据都会被填充成上面一行有实值的数据,这样的填充方法很不准确,比如说如果是时间序列数据的话,前一天和后一天的数据可能是有强相关的,可是有时相隔10天的数据差别是很大的。
#为此,有一个限定最大连续填充行数的方法limit,假如我想要限定往下填充只允许填充一行,则:
data_test.fillna(method='ffill',limit=1)
#可以发现,这种填充方法的话,44942行的“发博量”值就没有被连续填充
Out[110]:
11.数据保存¶
In [111]:
#好了基础的用法就介绍到这里,再补充一点:数据的保存
#假设现在的data是已处理好的干净的数据,想要保存下来待下一步操作,DataFrame的方法to_csv把数据保存为csv文件:
#括号里可以指定保存路径,路径有相对路径和绝对路径之分
#比如说代码data.to_csv('data.csv')会在你目前的工作目录下直接创建一个data.csv的文件保存数据,这是相对路径
#再比如说代码data.to_csv('/Users/apple/Desktop/data.csv')直接指定数据保存的绝对路径“/Users/apple/Desktop/data.csv”(Mac系统,路径与Windows不大一样)
data.to_csv('/Users/apple/Desktop/data.csv')
如上所说,这是第一篇,探究pandas更加全面的一些基础用法。关于使用pandas进行数据清理、转换、字符串的操作、数据的分组聚合运算以及结合图形进行数据探索的内容会在下一篇《如何溜溜地使用pandas操作数据(二)》中涉及到,请持续关注。