Pandas简介和使用

Pandas是基于NumPy的一个非常好用的库,正如名字一样,人见人爱。之所以如此,就在于不论是读取、处理数据,用它都非常简单。此前有一篇文章《别老扯什么Hadoop了,你的数据根本不够大》指出:只有在超过5TB数据量的规模下,Hadoop才是一个合理的技术选择。相对小一些的数据,我这两天发现使用Python + Pandas处理起来更加敏捷。

安装Pandas非常简单,只需要一行命令(当然你需要先安装pip):

1
pip install pandas

Pandas数据读取性能

Pandas提供了IO工具可以将大文件分块读取,测试了一下性能,完整加载9800万条数据也只需要263秒左右,还是相当不错了。

根据测试,对于不同数据量的数据读取时间如下:

1百万条 1千万条 1亿条
Data 1s 17s 263s

基本数据结构

Pandas有两种自己独有的基本数据结构。应该注意的是,它固然有着两种数据结构,因为它依然是python的一个库,所以,python中有的数据类型在这里依然适用,也同样还可以使用类自己定义数据类型。只不过,Pandas里面又定义了两种数据类型:Series和DataFrame,它们让数据操作更简单了。

以下操作都是基于:

1
2
from pandas import Series, DataFrame
import pandas as pd

Series

Series就如同列表一样,是一系列数据,每个数据对应一个索引值。比如这样一个列表:[9, 3, 8],如果跟索引值写到一起,就是:

data 9 3 8
index 0 1 2

这种样式我们已经熟悉了,不过,在有些时候,需要把它竖过来表示:

index data
0 9
1 3
2 8

上面两种,只是表现形式上的差别而已,而Series就是竖起来的list:

1
2
3
4
5
6
7
>>> s = Series([100, "PYTHON", "Soochow", "Qiwsir"])
>>> s
0 100
1 PYTHON
2 Soochow
3 Qiwsir
dtype: object

另外一点也很像列表,就是里面的元素的类型,由你任意决定(其实是由需要来决定)。

这里,我们实质上创建了一个Series对象,这个对象当然就有其属性和方法了。比如,下面的两个属性依次可以显示Series对象的数据值和索引:

1
2
3
4
5
>>> s.values
array([100, 'PYTHON', 'Soochow', 'Qiwsir'], dtype=object)
>>>
>>> s.index
RangeIndex(start=0, stop=4, step=1)

列表的索引只能是从0开始的整数,Series数据类型在默认情况下,其索引也是如此。不过,区别于列表的是,Series可以自定义索引:

1
2
3
4
5
6
7
8
9
10
>>> s2 = Series([100, "PYTHON", "Soochow", "Qiwsir"], \
... index=["mark", "title", "university", "name"])
>>> s2
mark 100
title PYTHON
university Soochow
name Qiwsir
dtype: object
>>> s2.index
Index([u'mark', u'title', u'university', u'name'], dtype='object')

每个元素都有了索引,就可以根据索引操作元素了。类似于list中的操作,根据索引查看其值和修改其值:

1
2
3
4
5
6
7
8
9
>>> s2["name"]
'Qiwsir'
>>> s2["name"] = "AOI"
>>> s2
mark 100
title PYTHON
university Soochow
name AOI
dtype: object

某种程度上,还有些类似dict操作数据;如果对此不太理解,可以看下面的例子。

前面定义Series对象的时候,用的是列表,即Series()方法的参数中,第一个列表就是其数据值,如果需要定义index,放在后面,依然是一个列表。除了这种方法之外,还可以用下面的方法定义Series对象:

1
2
3
4
5
6
7
>>> sd = {"python":8000, "c++":8100, "c":4000}
>>> s4 = Series(sd)
>>> s4
c 4000
c++ 8100
python 8000
dtype: int64

现在是否理解为什么前面那个类似dict了?因为本来就是可以这样定义的。

这时候,索引依然可以自定义。Pandas的优势在这里体现出来,如果自定义了索引,自定的索引会自动寻找原来的索引,如果一样的,就取原来索引对应的值,这个可以简称为“自动对齐”。

1
2
3
4
5
6
7
>>> s6 = Series(sd, index=["java", "python", "c++", "c"])
>>> s6
java NaN
python 8000.0
c++ 8100.0
c 4000.0
dtype: float64

在sd中,只有'python':8000, 'c++':8100, 'c':4000,没有"java",但是在索引参数中有,于是其它能够“自动对齐”的照搬原值,没有的那个"java",依然在新Series对象的索引中存在,并且自动为其赋值NaN。在Pandas中,如果没有值,都对齐赋给NaN

1
2
3
4
5
6
7
8
9
10
11
12
>>> pd.isnull(s6)
java True
python False
c++ False
c False
dtype: bool
>>> pd.notnull(s6)
java False
python True
c++ True
c True
dtype: bool

此外,Series对象也有同样的方法:

1
2
3
4
5
6
>>> s6.isnull()
java True
python False
c++ False
c False
dtype: bool

对于Series数据,也可以做类似下面的运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> s3 = Series([3, 9, 4, 7], index=['a', 'b', 'c', 'd'])
>>> s3
a 3
b 9
c 4
d 7
dtype: int64
>>> s3[s3 > 5]
b 9
d 7
dtype: int64
>>> s3 * 5
a 15
b 45
c 20
d 35
dtype: int64

DataFrame

DataFrame是一种二维的数据结构,非常接近于电子表格或者类似mysql数据库的形式。它的竖行称之为columns,横行跟前面的Series一样,称之为index,也就是说可以通过columns和index来确定一个主句的位置。

1
2
3
4
5
6
7
8
9
10
11
>>> import pandas as pd 
>>> from pandas import Series, DataFrame

>>> data = {"name":["yahoo","google","facebook"], \
... "marks":[200,400,800], "price":[9, 3, 7]}
>>> f1 = DataFrame(data)
>>> f1
marks name price
0 200 yahoo 9
1 400 google 3
2 800 facebook 7

这是定义一个DataFrame对象的常用方法——使用dict定义。字典的“键”(”name”,”marks”,”price”)就是DataFrame的columns的值(名称),字典中每个“键”的“值”是一个列表,它们就是那一竖列中的具体填充数据。上面的定义中没有确定索引,所以,按照惯例(Series中已经形成的惯例)就是从0开始的整数。从上面的结果中很明显表示出来,这就是一个二维的数据结构(类似excel或者mysql中的查看效果)。

上面的数据显示中,columns的顺序没有规定,就如同字典中键的顺序一样,但是在DataFrame中,columns跟字典键相比,有一个明显不同,就是其顺序可以被规定,像下面这样做:

1
2
3
4
5
6
>>> f2 = DataFrame(data, columns=['name','price','marks']) 
>>> f2
name price marks
0 yahoo 9 200
1 google 3 400
2 facebook 7 800

跟Series类似的,DataFrame数据的索引也能够自定义。

1
2
3
4
5
6
>>> f3 = DataFrame(data, columns=['name', 'price', 'marks', 'debt'], index=['a','b','c']) 
>>> f3
name price marks debt
a yahoo 9 200 NaN
b google 3 400 NaN
c facebook 7 800 NaN

还要注意观察上面的显示结果。因为在定义f3的时候,columns的参数中,比以往多了一项(‘debt’),但是这项在data这个字典中并没有,所以debt这一竖列的值都是空的,在Pandas中,空就用NaN来代表了。

定义DataFrame的方法,除了上面的之外,还可以使用“字典套字典”的方式。

1
2
3
4
5
6
>>> newdata = {"lang":{"firstline":"python","secondline":"java"}, "price":{"firstline":8000}} 
>>> f4 = DataFrame(newdata)
>>> f4
lang price
firstline python 8000.0
secondline java NaN

在字典中就规定好数列名称(第一层键)和每横行索引(第二层字典键)以及对应的数据(第二层字典值),也就是在字典中规定好了每个数据格子中的数据,没有规定的都是空。

1
2
3
4
5
>>> DataFrame(newdata, index=["firstline","secondline","thirdline"])
lang price
firstline python 8000.0
secondline java NaN
thirdline NaN NaN

如果额外确定了索引,就如同上面显示一样,除非在字典中有相应的索引内容,否则都是NaN。

前面定义了DataFrame数据(可以通过两种方法),它也是一种对象类型,比如变量f3引用了一个对象,它的类型是DataFrame。承接以前的思维方法:对象有属性和方法。

1
2
>>> f3.columns
Index([u'name', u'price', u'marks', u'debt'], dtype='object')

DataFrame对象的columns属性,能够显示现有的columns名称。并且,还能用下面类似字典的方式,得到某竖列的全部内容(当然包含索引):

1
2
3
4
5
>>> f3['name'] 
a yahoo
b google
c facebook
Name: name, dtype: object

这是什么?这其实就是一个Series,或者说,可以将DataFrame理解为是由一个一个的Series组成的。

对于没有数值的那一列,下面的操作是统一给那一列赋值:

1
2
3
4
5
6
>>> f3['debt'] = 89.2 
>>> f3
name price marks debt
a yahoo 9 200 89.2
b google 3 400 89.2
c facebook 7 800 89.2

除了能够统一赋值之外,还能够“点对点”添加数值,结合前面的Series,既然DataFrame对象的每竖列都是一个Series对象,那么可以先定义一个Series对象,然后把它放到DataFrame对象中。如下:

1
2
>>> sdebt = Series([2.2, 3.3], index=["a","c"])    #注意索引 
>>> f3['debt'] = sdebt

将Series对象(sdebt变量所引用) 赋给f3[‘debt’]列,Pandas的一个重要特性——自动对齐——在这里起做用了,在Series中,只有两个索引(”a”,”c”),它们将和DataFrame中的索引自动对齐。于是有:

1
2
3
4
5
>>> f3
name price marks debt
a yahoo 9 200 2.2
b google 3 400 NaN
c facebook 7 800 3.3

自动对齐之后,没有被复制的依然保持NaN。

Pandas使用

由于篇幅限制,不能讲pandas所有内容完全详尽描述,只是做一个简单的介绍,在实践中使用如果遇到问题,建议参考相关文档或者google来解决(不要用baidu)。

读取csv文件

普通办法读取

最简单、最直接的就是open()打开文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> with open("./marks.csv") as f:
... for line in f:
... print line
...
name,physics,python,math,english

Google,100,100,25,12

Facebook,45,54,44,88

Twitter,54,76,13,91

Yahoo,54,452,26,100

此方法可以,但略显麻烦。python中还有一个csv的标准库:

1
2
3
>>> import csv 
>>> dir(csv)
['Dialect', 'DictReader', 'DictWriter', 'Error', 'QUOTE_ALL', 'QUOTE_MINIMAL', 'QUOTE_NONE', 'QUOTE_NONNUMERIC', 'Sniffer', 'StringIO', '_Dialect', '__all__', '__builtins__', '__doc__', '__file__', '__name__', '__package__', '__version__', 'excel', 'excel_tab', 'field_size_limit', 'get_dialect', 'list_dialects', 're', 'reader', 'reduce', 'register_dialect', 'unregister_dialect', 'writer']

从上面结果可以看出,csv模块提供的属性和方法。仅仅就读取本例子中的文件:

1
2
3
4
5
6
7
8
9
10
>>> import csv 
>>> csv_reader = csv.reader(open("./marks.csv"))
>>> for row in csv_reader:
... print row
...
['name', 'physics', 'python', 'math', 'english']
['Google', '100', '100', '25', '12']
['Facebook', '45', '54', '44', '88']
['Twitter', '54', '76', '13', '91']
['Yahoo', '54', '452', '26', '100']

算是稍有改善。

用Pandas读取

我们来看一下pandas的效果:

1
2
3
4
5
6
7
8
>>> import pandas as pd
>>> marks = pd.read_csv("./marks.csv")
>>> marks
name physics python math english
0 Google 100 100 25 12
1 Facebook 45 54 44 88
2 Twitter 54 76 13 91
3 Yahoo 54 452 26 100

以上结果就是一个DataFrame数据对象。

还有另外一种方法:

1
2
3
4
5
6
>>> pd.read_table("./marks.csv", sep=",")
name physics python math english
0 Google 100 100 25 12
1 Facebook 45 54 44 88
2 Twitter 54 76 13 91
3 Yahoo 54 452 26 100

如果你有足够的好奇心来研究这个名叫DataFrame的对象,可以这样:

1
2
>>> dir(marks)
['T', '_AXIS_ALIASES', '_AXIS_NAMES', '_AXIS_NUMBERS', '__add__', '__and__', '__array__', '__array_wrap__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dict__', '__div__', '__doc__', '__eq__', '__floordiv__', '__format__', '__ge__', '__getattr__', '__getattribute__', '__getitem__', '__getstate__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__module__', '__mul__', '__ne__', '__neg__', '__new__', '__nonzero__', '__or__', '__pow__', '__radd__', '__rdiv__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rmul__', '__rpow__', '__rsub__', '__rtruediv__', '__setattr__', '__setitem__', '__setstate__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__weakref__', '__xor__', '_agg_by_level', '_align_frame', '_align_series', '_apply_broadcast', '_apply_raw', '_apply_standard', '_auto_consolidate', '_bar_plot', '_boolean_set', '_box_item_values', '_clear_item_cache', '_combine_const', '_combine_frame', '_combine_match_columns', '_combine_match_index', '_combine_series', '_combine_series_infer', '_compare_frame', '_consolidate_inplace', '_constructor', '_count_level', '_cov_helper', '_data', '_default_stat_axis', '_expand_axes', '_from_axes', '_get_agg_axis', '_get_axis', '_get_axis_name', '_get_axis_number', '_get_item_cache', '_get_numeric_data', '_getitem_array', '_getitem_multilevel', '_helper_csvexcel', '_het_axis', '_indexed_same', '_init_dict', '_init_mgr', '_init_ndarray', '_is_mixed_type', '_item_cache', '_ix', '_join_compat', '_reduce', '_reindex_axis', '_reindex_columns', '_reindex_index', '_reindex_with_indexers', '_rename_columns_inplace', '_rename_index_inplace', '_sanitize_column', '_series', '_set_axis', '_set_item', '_set_item_multiple', '_shift_indexer', '_slice', '_unpickle_frame_compat', '_unpickle_matrix_compat', '_verbose_info', '_wrap_array', 'abs', 'add', 'add_prefix', 'add_suffix', 'align', 'append', 'apply', 'applymap', 'as_matrix', 'asfreq', 'astype', 'axes', 'boxplot', 'clip', 'clip_lower', 'clip_upper', 'columns', 'combine', 'combineAdd', 'combineMult', 'combine_first', 'consolidate', 'convert_objects', 'copy', 'corr', 'corrwith', 'count', 'cov', 'cummax', 'cummin', 'cumprod', 'cumsum', 'delevel', 'describe', 'diff', 'div', 'dot', 'drop', 'drop_duplicates', 'dropna', 'dtypes', 'duplicated', 'fillna', 'filter', 'first_valid_index', 'from_csv', 'from_dict', 'from_items', 'from_records', 'get', 'get_dtype_counts', 'get_value', 'groupby', 'head', 'hist', 'icol', 'idxmax', 'idxmin', 'iget_value', 'index', 'info', 'insert', 'irow', 'iteritems', 'iterkv', 'iterrows', 'ix', 'join', 'last_valid_index', 'load', 'lookup', 'mad', 'max', 'mean', 'median', 'merge', 'min', 'mul', 'ndim', 'pivot', 'pivot_table', 'plot', 'pop', 'prod', 'product', 'quantile', 'radd', 'rank', 'rdiv', 'reindex', 'reindex_axis', 'reindex_like', 'rename', 'rename_axis', 'reorder_levels', 'reset_index', 'rmul', 'rsub', 'save', 'select', 'set_index', 'set_value', 'shape', 'shift', 'skew', 'sort', 'sort_index', 'sortlevel', 'stack', 'std', 'sub', 'sum', 'swaplevel', 'tail', 'take', 'to_csv', 'to_dict', 'to_excel', 'to_html', 'to_panel', 'to_records', 'to_sparse', 'to_string', 'to_wide', 'transpose', 'truncate', 'unstack', 'values', 'var', 'xs']

一个一个浏览一下,通过名字可以直到那个方法或者属性的大概,然后就可以根据你的喜好和需要,试一试:

1
2
3
4
5
6
>>> marks.index
Int64Index([0, 1, 2, 3], dtype=int64)
>>> marks.columns
Index([name, physics, python, math, english], dtype=object)
>>> marks['name'][1]
'Facebook'

下面几个操作,也是常用到的,并且秉承了python的一贯方法:

1
2
3
4
5
6
7
8
9
10
11
12
>>> marks[:1]
name physics python math english
0 Google 100 100 25 12
>>> marks[1:2]
name physics python math english
1 Facebook 45 54 44 88
>>> marks["physics"]
0 100
1 45
2 54
3 54
Name: physics

可以说,当你已经掌握了通过dir()和help()查看对象的方法和属性时,就已经掌握了pandas的用法,其实何止pandas,其它对象都是如此。

一个pandas实例

上周一个朋友提出了一个简单的需求:对于百万条数量级的csv文件做一个简单的数据分析,在此非常感谢他让我有机会接触到了pandas,话不多说,上代码:

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
30
31
import pandas as pd
import datetime, csv

def get_data(rows, key, count):
data_max, data_min = rows[key].max(), rows[key].min()
data_avg = (rows[key].sum() / float(count)) if count != 0 else 0
return data_max, data_min, data_avg

if __name__ == "__main__":
starttime = datetime.datetime.now()
data = pd.read_csv("new_colla.csv")
with open('degree_year.csv','ab+') as ft:
writer = csv.writer(ft)
for i in range(n):
for j in range(m):
rows = data[(data.degree1==i) & (data.degree2==j)]

# Get valuable data
age, count = (i, j), rows.colla_time.count()
if count == 0:
continue
colla_time_max, colla_time_min, colla_time_avg = get_data(rows, "colla_time", count)
coarticle_num_max, coarticle_num_min, coarticle_num_avg = get_data(rows, "coarticle_num", count)

# Write file
write_obj = [age, colla_time_max, colla_time_min, colla_time_avg, \
coarticle_num_max, coarticle_num_min, coarticle_num_avg, count]
writer.writerow(write_obj)

endtime = datetime.datetime.now()
print (endtime - starttime).seconds