和大熊貓們(Pandas)一起游戲吧!
Pandas是Python的一個用於數據分析的庫: http://pandas.pydata.org
API速查:http://pandas.pydata.org/pandas-docs/stable/api.html
基於NumPy,SciPy的功能,在其上補充了大量的數據操作(Data Manipulation)功能。
統計、分組、排序、透視表自由轉換,如果你已經很熟悉結構化數據庫(RDBMS)與Excel的功能,就會知道Pandas有過之而無不及!
0. 上手玩:Why Pandas?
普通的程序員看到一份數據會怎么做?
import codecs
import requests
import numpy as np
import scipy as sp
import scipy.stats as spstat
import pandas as pd
import datetime
import json
r = requests.get("http://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data")
with codecs.open('S1EP3_Iris.txt','w',encoding='utf-8') as f:
f.write(r.text)
with codecs.open('S1EP3_Iris.txt','r',encoding='utf-8') as f:
lines = f.readlines()
for idx,line in enumerate(lines):
print line,
if idx==10:
break
Pandas的意義就在於
快速的識別結構化數據
import pandas as pd
irisdata = pd.read_csv('S1EP3_Iris.txt',header = None, encoding='utf-8')
#irisdata
快速的操作元數據
cnames = ['sepal_length','sepal_width','petal_length','petal_width','class']
irisdata.columns = cnames
#irisdata
快速過濾
#irisdata[irisdata['petal_width']==irisdata.petal_width.max()]
irisdata[irisdata['sepal_length']==irisdata.sepal_length.max()]
快速切片
#irisdata.iloc[::30,:2]
irisdata.iloc[::40,0:3]
快速統計
#print irisdata['class'].value_counts()
for x in xrange(4):
s = irisdata.iloc[:,x]
print '{0:<12}'.format(s.name.upper()), " Statistics: ", \
'{0:>5} {1:>5} {2:>5} {3:>5}'.format(s.max(), s.min(), round(s.mean(),2),round(s.std(),2))
快速“MapReduce”
slogs = lambda x:sp.log(x)*x
entpy = lambda x:sp.exp((slogs(x.sum())-x.map(slogs).sum())/x.sum())
irisdata.groupby('class').agg(entpy)
1. 歡迎來到大熊貓世界
Pandas的重要數據類型
- DataFrame(二維表)
- Series(一維序列)
- Index(行索引,行級元數據)
1.1 Series:pandas的長槍(數據表中的一列或一行,觀測向量,一維數組...)
數據世界中對於任意一個個體的全面觀測,或者對於任意一組個體某一屬性的觀測,全部可以抽象為Series的概念。
用值構建一個Series:
由默認index和values組成。
Series1 = pd.Series(np.random.randn(4))
print Series1,type(Series1)
print Series1.index
print Series1.values
Series支持過濾的原理就如同NumPy:
print Series1>0
print Series1[Series1>0]
當然也支持Broadcasting:
print Series1*2
print Series1+5
以及Universal Function:
print np.exp(Series1)
#NumPy Universal Function
f_np = np.frompyfunc(lambda x:np.exp(x*2+5),1,1)
print f_np(Series1)
在序列上就使用行標,而不是創建一個2列的數據表,能夠輕松辨別哪里是數據,哪里是元數據:
Series2 = pd.Series(Series1.values,index=['norm_'+unicode(i) for i in xrange(4)])
print Series2,type(Series2)
print Series2.index
print type(Series2.index)
print Series2.values
雖然行是有順序的,但是仍然能夠通過行級的index來訪問到數據:
(當然也不盡然像Ordered Dict,因為行索引甚至可以重復,不推薦重復的行索引不代表不能用)
print Series2[['norm_0','norm_3']]
print 'norm_0' in Series2
print 'norm_6' in Series2
默認行索引就像行號一樣:
print Series1.index
從Key不重復的Ordered Dict或者從Dict來定義Series就不需要擔心行索引重復:
Series3_Dict = {"Japan":"Tokyo","S.Korea":"Seoul","China":"Beijing"}
Series3_pdSeries = pd.Series(Series3_Dict)
print Series3_pdSeries
print Series3_pdSeries.values
print Series3_pdSeries.index
與Dict區別一: 有序
Series4_IndexList = ["Japan","China","Singapore","S.Korea"]
Series4_pdSeries = pd.Series( Series3_Dict ,index = Series4_IndexList)
print Series4_pdSeries
print Series4_pdSeries.values
print Series4_pdSeries.index
print Series4_pdSeries.isnull()
print Series4_pdSeries.notnull()
與Dict區別二: index內值可以重復,盡管不推薦。
Series5_IndexList = ['A','B','B','C']
Series5 = pd.Series(Series1.values,index = Series5_IndexList)
print Series5
print Series5[['B','A']]
整個序列級別的元數據信息:name
當數據序列以及index本身有了名字,就可以更方便的進行后續的數據關聯啦!
print Series4_pdSeries.name
print Series4_pdSeries.index.name
Series4_pdSeries.name = "Capital Series"
Series4_pdSeries.index.name = "Nation"
print Series4_pdSeries
pd.DataFrame(Series4_pdSeries)
1.2 DataFrame:pandas的戰錘(數據表,二維數組)
Series的有序集合,就像R的DataFrame一樣方便。
仔細想想,絕大部分的數據形式都可以表現為DataFrame。
從NumPy二維數組、從文件或者從數據庫定義:數據雖好,勿忘列名
dataNumPy = np.asarray([('Japan','Tokyo',4000),\
('S.Korea','Seoul',1300),('China','Beijing',9100)])
DF1 = pd.DataFrame(dataNumPy,columns=['nation','capital','GDP'])
DF1
等長的列數據保存在一個字典里(JSON):很不幸,字典key是無序的
dataDict = {'nation':['Japan','S.Korea','China'],\
'capital':['Tokyo','Seoul','Beijing'],'GDP':[4900,1300,9100]}
DF2 = pd.DataFrame(dataDict)
DF2
從另一個DataFrame定義DataFrame:啊,強迫症犯了!
DF21 = pd.DataFrame(DF2,columns=['nation','capital','GDP'])
DF21
DF22 = pd.DataFrame(DF2,columns=['nation','capital','GDP'],index = [2,0,1])
DF22
從DataFrame中取出列?兩種方法(與JavaScript完全一致!)
- '.'的寫法容易與其他預留關鍵字產生沖突
- '[ ]'的寫法最安全。
print DF22.nation
print DF22.capital
print DF22['GDP']
從DataFrame中取出行?(至少)兩種方法:
print DF22[0:1] #給出的實際是DataFrame
print DF22.ix[0] #通過對應Index給出行
像NumPy切片一樣的終極招式:iloc
print DF22.iloc[0,:]
print DF22.iloc[:,0]
聽說你從Alter Table地獄來,大熊貓笑了
然而動態增加列無法用"."的方式完成,只能用"[ ]"
DF22['population'] = [1600,130,55]
DF22['region'] = 'East_Asian'
DF22
1.3 Index:pandas進行數據操縱的鬼牌(行級索引)
行級索引是
- 元數據
- 可能由真實數據產生,因此可以視作數據
- 可以由多重索引也就是多個列組合而成
- 可以和列名進行交換,也可以進行堆疊和展開,達到Excel透視表效果
Index有四種...哦不,很多種寫法,一些重要的索引類型包括
- pd.Index(普通)
- Int64Index(數值型索引)
- MultiIndex(多重索引,在數據操縱中更詳細描述)
- DatetimeIndex(以時間格式作為索引)
- PeriodIndex (含周期的時間格式作為索引)
直接定義普通索引,長得就和普通的Series一樣
index_names = ['a','b','c']
Series_for_Index = pd.Series(index_names)
print pd.Index(index_names)
print pd.Index(Series_for_Index)
可惜Immutable,牢記!
index_names = ['a','b','c']
index0 = pd.Index(index_names)
print index0.get_values()
index0[2] = 'd'
扔進去一個含有多元組的List,就有了MultiIndex
可惜,如果這個List Comprehension改成小括號,就不對了。
#print [('Row_'+str(x+1),'Col_'+str(y+1)) for x in xrange(4) for y in xrange(4)]
multi1 = pd.Index([('Row_'+str(x+1),'Col_'+str(y+1)) for x in xrange(4) for y in xrange(4)])
multi1.name = ['index1','index2']
print multi1
對於Series來說,如果擁有了多重Index,數據,變形!
下列代碼說明:
- 二重MultiIndex的Series可以unstack()成DataFrame
- DataFrame可以stack成擁有二重MultiIndex的Series
data_for_multi1 = pd.Series(xrange(0,16),index=multi1)
data_for_multi1
data_for_multi1.unstack()
data_for_multi1.unstack().stack()
我們來看一下非平衡數據的例子:
Row_1,2,3,4和Col_1,2,3,4並不是全組合的。
multi2 = pd.Index([('Row_'+str(x),'Col_'+str(y+1)) \
for x in xrange(5) for y in xrange(x)])
multi2
data_for_multi2 = pd.Series(np.arange(10),index = multi2)
data_for_multi2
data_for_multi2.unstack()
data_for_multi2.unstack().stack()
DateTime標准庫如此好用,你值得擁有
dates = [datetime.datetime(2015,1,1),datetime.datetime(2015,1,8),datetime.datetime(2015,1,30)]
pd.DatetimeIndex(dates)
如果你不僅需要時間格式統一,時間頻率也要統一的話
periodindex1 = pd.period_range('2015-01','2015-04',freq='M')
print periodindex1
月級精度和日級精度如何轉換?
有的公司統一以1號代表當月,有的公司統一以最后一天代表當月,轉化起來很麻煩,可以asfreq
print periodindex1.asfreq('D',how='start')
print periodindex1.asfreq('D',how='end')
最后的最后,我要真正把兩種頻率的時間精度匹配上?
periodindex_mon = pd.period_range('2015-01','2015-03',freq='M').asfreq('D',how='start')
periodindex_day = pd.period_range('2015-01-01','2015-03-31',freq='D')
print periodindex_mon
print periodindex_day
粗粒度數據+reindex+ffill/bfill
#print pd.Series(periodindex_mon,index=periodindex_mon).reindex(periodindex_day)
full_ts = pd.Series(periodindex_mon,index=periodindex_mon).reindex(periodindex_day)
full_ts
full_ts = pd.Series(periodindex_mon,index=periodindex_mon).reindex(periodindex_day,method='ffill')
full_ts
關於索引,方便的操作有?
前面描述過了,索引有序,重復,但一定程度上又能通過key來訪問,也就是說,某些集合操作都是可以支持的。
index1 = pd.Index(['A','B','B','C','C'])
index2 = pd.Index(['C','D','E','E','F'])
index3 = pd.Index(['B','C','A'])
print index1.append(index2)
print index1.difference(index2)
print index1.intersection(index2)
print index1.union(index2) # Support unique-value Index well
print index1.isin(index2)
print index1.delete(2)
print index1.insert(0,'K') # Not suggested
print index3.drop('A') # Support unique-value Index well
print index1.is_monotonic,index2.is_monotonic,index3.is_monotonic
print index1.is_unique,index2.is_unique,index3.is_unique
2. 大熊貓世界來去自如:Pandas的I/O
老生常談,從基礎來看,我們仍然關心pandas對於與外部數據是如何交互的。
2.1 結構化數據輸入輸出
- read_csv與to_csv 是一對輸入輸出的工具,read_csv直接返回pandas.DataFrame,而to_csv只要執行命令即可寫文件
- read_table:功能類似
- read_fwf:操作fixed width file
- read_excel與to_excel方便的與excel交互
還記得剛開始的例子嗎?
- header 表示數據中是否存在列名,如果在第0行就寫就寫0,並且開始讀數據時跳過相應的行數,不存在可以寫none
- names 表示要用給定的列名來作為最終的列名
- encoding 表示數據集的字符編碼,通常而言一份數據為了方便的進行文件傳輸都以utf-8作為標准
提問:下列例子中,header=4,names=cnames時,究竟會讀到怎樣的數據?
print cnames
irisdata = pd.read_csv('S1EP3_Iris.txt',header = None, names = cnames,\
encoding='utf-8')
irisdata[::30]
希望了解全部參數的請移步API:
http://pandas.pydata.org/pandas-docs/stable/generated/pandas.read_csv.html#pandas.read_csv
這里介紹一些常用的參數:
讀取處理:
- skiprows:跳過一定的行數
- nrows:僅讀取一定的行數
- skipfooter:尾部有固定的行數永不讀取
- skip_blank_lines:空行跳過
內容處理:
- sep/delimiter:分隔符很重要,常見的有逗號,空格和Tab('\t')
- na_values:指定應該被當作na_values的數值
- thousands:處理數值類型時,每千位分隔符並不統一 (1.234.567,89或者1,234,567.89都可能),此時要把字符串轉化為數字需要指明千位分隔符
收尾處理:
- index_col:將真實的某列(列的數目,甚至列名)當作index
- squeeze:僅讀到一列時,不再保存為pandas.DataFrame而是pandas.Series
2.1.x Excel ... ?
對於存儲着極為規整數據的Excel而言,其實是沒必要一定用Excel來存,盡管Pandas也十分友好的提供了I/O接口。
irisdata.to_excel('S1EP3_irisdata.xls',index = None,encoding='utf-8')
irisdata_from_excel = pd.read_excel('S1EP3_irisdata.xls',header=0, encoding='utf-8')
irisdata_from_excel[::30]
唯一重要的參數:sheetname=k,標志着一個excel的第k個sheet頁將會被取出。(從0開始)
2.2 半結構化數據
JSON:網絡傳輸中常用的一種數據格式。
仔細看一下,實際上這就是我們平時收集到異源數據的風格是一致的:
- 列名不能完全匹配
- 關聯鍵可能並不唯一
- 元數據被保存在數據里
json_data = [{'name':'Wang','sal':50000,'job':'VP'},\
{'name':'Zhang','job':'Manager','report':'VP'},\
{'name':'Li','sal':5000,'report':'Manager'}]
data_employee = pd.read_json(json.dumps(json_data))
data_employee_ri = data_employee.reindex(columns=['name','job','sal','report'])
data_employee_ri
2.3 數據庫連接流程(Optional)
使用下列包,通過數據庫配置建立Connection
- pymysql
- pyODBC
- cx_Oracle
通過pandas.read_sql_query,read_sql_table,to_sql進行數據庫操作。
Python與數據庫的交互方案有很多種,從數據分析師角度看pandas方案比較適合,之后的講義中會結合SQL語法進行講解。
進行數據庫連接首先你需要類似的這樣一組信息:
IP = '127.0.0.1'
us = 'root'
pw = '123456'
舉例說明如果是MySQL:
import pymysql
import pymysql.cursors
connection = pymysql.connect(host=IP,\
user=us,\
password=pw,\
charset='utf8mb4',\
cursorclass=pymysql.cursors.DictCursor)
#pd.read_sql_query("sql",connection)
#df.to_sql('tablename',connection,flavor='mysql')
3. 深入Pandas數據操縱
在第一部分的基礎上,數據會有更多種操縱方式:
- 通過列名、行index來取數據,結合ix、iloc靈活的獲取數據的一個子集(第一部分已經介紹)
- 按記錄拼接(就像Union All)或者關聯(join)
- 方便的自定義函數映射
- 排序
- 缺失值處理
- 與Excel一樣靈活的數據透視表(在第四部分更詳細介紹)
3.1 數據整合:方便靈活
3.1.1 橫向拼接:直接DataFrame
pd.DataFrame([np.random.rand(2),np.random.rand(2),np.random.rand(2)],columns=['C1','C2'])
3.1.2 橫向拼接:Concatenate
pd.concat([data_employee_ri,data_employee_ri,data_employee_ri])
pd.concat([data_employee_ri,data_employee_ri,data_employee_ri],ignore_index=True)
3.1.3 縱向拼接:Merge
根據數據列關聯,使用on關鍵字
- 可以指定一列或多列
- 可以使用left_on和right_on
pd.merge(data_employee_ri,data_employee_ri,on='name')
pd.merge(data_employee_ri,data_employee_ri,on=['name','job'])
根據index關聯,可以直接使用left_index和right_index
data_employee_ri.index.name = 'index1'
pd.merge(data_employee_ri,data_employee_ri,\
left_index='index1',right_index='index1')
TIPS: 增加how關鍵字,並指定
- how = 'inner'
- how = 'left'
- how = 'right'
- how = 'outer'
結合how,可以看到merge基本再現了SQL應有的功能,並保持代碼整潔。
DF31xA = pd.DataFrame({'name':[u'老王',u'老張',u'老李'],'sal':[5000,3000,1000]})
DF31xA
DF31xB = pd.DataFrame({'name':[u'老王',u'老劉'],'job':['VP','Manager']})
DF31xB
how='left': 保留左表信息
pd.merge(DF31xA,DF31xB,on='name',how='left')
how='right': 保留右表信息
pd.merge(DF31xA,DF31xB,on='name',how='right')
how='inner': 保留兩表交集信息,這樣盡量避免出現缺失值
pd.merge(DF31xA,DF31xB,on='name',how='inner')
how='outer': 保留兩表並集信息,這樣會導致缺失值,但最大程度的整合了已有信息
pd.merge(DF31xA,DF31xB,on='name',how='outer')
3.2 數據清洗三劍客
接下來的三個功能,map,applymap,apply,功能,是絕大多數數據分析師在數據清洗這一步驟中的必經之路。
他們分別回答了以下問題:
- 我想根據一列數據新做一列數據,怎么辦?(Series->Series)
- 我想根據整張表的數據新做整張表,怎么辦? (DataFrame->DataFrame)
- 我想根據很多列的數據新做一列數據,怎么辦? (DataFrame->Series)
不要再寫什么for循環了!改變思維,提高編碼和執行效率
dataNumPy32 = np.asarray([('Japan','Tokyo',4000),('S.Korea','Seoul',1300),('China','Beijing',9100)])
DF32 = pd.DataFrame(dataNumPy,columns=['nation','capital','GDP'])
DF32
map: 以相同規則將一列數據作一個映射,也就是進行相同函數的處理
def GDP_Factorize(v):
fv = np.float64(v)
if fv > 6000.0:
return 'High'
elif fv < 2000.0:
return 'Low'
else:
return 'Medium'
DF32['GDP_Level'] = DF32['GDP'].map(GDP_Factorize)
DF32['NATION'] = DF32.nation.map(str.upper)
DF32
類似的功能還有applymap,可以對一個dataframe里面每一個元素像map那樣全局操作
DF32.applymap(lambda x: float(x)*2 if x.isdigit() else x.upper())
apply則可以對一個DataFrame操作得到一個Series
他會有點像我們后面介紹的agg,但是apply可以按行操作和按列操作,用axis控制即可。
DF32.apply(lambda x:x['nation']+x['capital']+'_'+x['GDP'],axis=1)
3.3 數據排序
- sort: 按一列或者多列的值進行行級排序
- sort_index: 根據index里的取值進行排序,而且可以根據axis決定是重排行還是列
dataNumPy33 = np.asarray([('Japan','Tokyo',4000),('S.Korea','Seoul',1300),('China','Beijing',9100)])
DF33 = pd.DataFrame(dataNumPy33,columns=['nation','capital','GDP'])
DF33
DF33.sort(['capital','nation'])
DF33.sort('GDP',ascending=False)
DF33.sort('GDP').sort(ascending=False)
DF33.sort_index(axis=1,ascending=True)
一個好用的功能:Rank
DF33
DF33.rank()
DF33.rank(ascending=False)
注意tied data(相同值)的處理:
- method = 'average'
- method = 'min'
- method = 'max'
- method = 'first'
DF33x = pd.DataFrame({'name':[u'老王',u'老張',u'老李',u'老劉'],'sal':np.array([5000,3000,5000,9000])})
DF33x
DF33x.rank()默認使用method='average',兩條數據相等時,處理排名時大家都用平均值
DF33x.sal.rank()
method='min',處理排名時大家都用最小值
DF33x.sal.rank(method='min')
method='max',處理排名時大家都用最大值
DF33x.sal.rank(method='max')
method='first',處理排名時誰先出現就先給誰較小的數值。
DF33x.sal.rank(method='first')
3.4 缺失數據處理
DF34 = data_for_multi2.unstack()
DF34
忽略缺失值:
DF34.mean(skipna=True)
DF34.mean(skipna=False)
如果不想忽略缺失值的話,就需要祭出fillna了:
DF34
DF34.fillna(0).mean(axis=1,skipna=False)
4. “一組”大熊貓:Pandas的groupby
groupby的功能類似SQL的group by關鍵字:
Split-Apply-Combine
- Split,就是按照規則分組
- Apply,通過一定的agg函數來獲得輸入pd.Series返回一個值的效果
- Combine,把結果收集起來
Pandas的groupby的靈活性:
- 分組的關鍵字可以來自於index,也可以來自於真實的列數據
- 分組規則可以通過一列或者多列
from IPython.display import Image
Image(filename="S1EP3_group.png")
分組的具體邏輯
irisdata_group = irisdata.groupby('class')
irisdata_group
for level,subsetDF in irisdata_group:
print level
print subsetDF[::20]
分組可以快速實現MapReduce的邏輯
- Map: 指定分組的列標簽,不同的值就會被扔到不同的分組處理
- Reduce: 輸入多個值,返回一個值,一般可以通過agg實現,agg能接受一個函數
irisdata.groupby('class').agg(\
lambda x:((x-x.mean())**3).sum()*(len(x)-0.0)/\
(len(x)-1.0)/(len(x)-2.0)/(x.std()*np.sqrt((len(x)-0.0)/(len(x)-1.0)))**3 if len(x)>2 else None)
irisdata.groupby('class').agg(spstat.skew)
匯總之后的廣播操作
在OLAP數據庫上,為了避免groupby+join的二次操作,提出了sum()over(partition by)的開窗操作。
在Pandas中,這種操作能夠進一步被transform所取代。
pd.concat([irisdata,irisdata.groupby('class').transform('mean')],axis=1)[::20]
產生 MultiIndex(多列分組)后的數據透視表操作
一般來說,多列groupby的一個副作用就是.groupby().agg()之后你的行index已經變成了一個多列分組的分級索引。
如果我們希望達到Excel的數據透視表的效果,行和列的索引自由交換,達到統計目的,究竟應該怎么辦呢?
factor1 = np.random.randint(0,3,50)
factor2 = np.random.randint(0,2,50)
factor3 = np.random.randint(0,3,50)
values = np.random.randn(50)
hierindexDF = pd.DataFrame({'F1':factor1,'F2':factor2,'F3':factor3,'F4':values})
hierindexDF
hierindexDF_gbsum = hierindexDF.groupby(['F1','F2','F3']).sum()
hierindexDF_gbsum
觀察Index:
hierindexDF_gbsum.index
unstack:
- 無參數時,把最末index置換到column上
- 有數字參數時,把指定位置的index置換到column上
- 有列表參數時,依次把特定位置的index置換到column上
hierindexDF_gbsum.unstack()
hierindexDF_gbsum.unstack(0)
hierindexDF_gbsum.unstack(1)
hierindexDF_gbsum.unstack([2,0])
更進一步的,stack的功能是和unstack對應,把column上的多級索引換到index上去
hierindexDF_gbsum.unstack([2,0]).stack([1,2])
