Pandas中兼并数组和字典功能的Series

作者 : RY    标签: series pandas

Pandas中的Series对象提供了数组和字典的功能,在简要介绍其用法之后,我们将深究一下Series对象的实现原理。

In [1]:
def C(*args, **kw):
    from itertools import izip_longest
    gap = kw.pop("gap", 5)
    results = []
    for item in args:
       if isinstance(item, tuple):
           results.append("\n".join(unicode(row) for row in item).split("\n"))
           
    for i, item in enumerate(results):
       width = max(len(row) for row in item)
       results[i].insert(1, "-"*width)        
       results[i] = [row.ljust(width+gap) for row in item]
    col_widths = [max(map(len, column)) for column in results]
   
    for row in izip_longest(*results):
       row = [x if x is not None else (" " * col_widths[i]) for i, x in enumerate(row)]
       print "".join(row)

兼并数组和字典功能的Series

In [2]:
import pandas as pd

Series对象本质上是一个NumPy的数组,因此NumPy的数组处理函数可以直接对Series进行处理。但是Series除了可以使用位置作为下标存取元素之外,还可以使用标签下标存取元素,这一点和字典相似。每个Series对象实际上都由两个数组组成:

  • index: 它是从NumPy数组继承的Index对象,保存标签信息。
  • values: 保存值的NumPy数组。

下面创建一个Series对象,并查看其两个属性:

In [3]:
s = pd.Series([1,2,3,4,5], index=["a","b","c","d","e"])
print s.index
print s.values
Index([a, b, c, d, e], dtype=object)
[1 2 3 4 5]

Series的下标存取,同时支持位置和标签两种形式:

In [4]:
print s[2], s["d"]
3 4

Series也支持位置切片和标签切片。位置切片遵循Python的切片规则,包括起始位置,但不包括结束位置;但标签切片则同时包括起始标签和结束标签。之所以如此设计是因为在使用标签切片时,通常我们不知道标签的顺序,如果不包含结束标签,很难确定结束标签的前一个标签是什么。

In [5]:
C((u"s[1:3]", s[1:3]), (u"s['b':'d']", s['b':'d']))
s[1:3]     s['b':'d']     
------     ----------     
b    2     b    2         
c    3     c    3         
           d    4         

和NumPy数组一样,Series也可以使用一个位置列表或者位置数组进行存取;同时还可以使用标签列表和标签数组。

In [6]:
C((u"s[[1,3,2]]", s[[1,3,2]]), (u"s[['b','d','c']]", s[['b','d','c']]))
s[[1,3,2]]     s[['b','d','c']]     
----------     ----------------     
b    2         b    2               
d    4         d    4               
c    3         c    3               

可以看出Series同时具有数组和字典的功能,因此它也支持一些字典的方法,例如Series.iteritems()

In [7]:
print list(s.iteritems())
[('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]

Series魔法都在Index里

Index对象也是ndarray的派生类,values属性可以获得ndarray数组:

In [8]:
index = s.index
print index.__class__.mro()
index.values
Out[8]:
[<class 'pandas.core.index.Index'>, <type 'numpy.ndarray'>, <type 'object'>]
array(['a', 'b', 'c', 'd', 'e'], dtype=object)

Index可以当作一维数组,支持所有的数组下标操作:

In [9]:
print index[[1, 3]]
print index[index > 'c']
print index[1::2]
Index([b, d], dtype=object)
Index([d, e], dtype=object)
Index([b, d], dtype=object)

Index也具有字典的映射功能,它将数组中的值映射到其位置:

  • Index.get_loc(value): 获得单个值value的下标
  • Index.get_indexer(values): 获得一组值values的下标,当值不存在时,得到-1
In [10]:
print index.get_loc('c')
index.get_indexer(['a', 'c', 'z'])
Out[10]:
2
array([ 0,  2, -1])

Index的魔法在Engine里

Index对象的字典功能由其中的Engine对象提供:

In [11]:
e = s.index._engine
print e
<pandas.index.ObjectEngine object at 0x2fd1c50>
In [12]:
e.get_loc('b')
Out[12]:
1
In [13]:
e.get_indexer(np.array(['a', 'd', 'e', 'z'], 'O'))
Out[13]:
array([ 0,  3,  4, -1])

Engine中的HashTable

Engine对象的字典功能由mapping提供:

In [14]:
ht = e.mapping
print ht
<pandas.hashtable.PyObjectHashTable object at 0x2543170>
In [15]:
ht.get_item('d')
Out[15]:
3
In [16]:
ht.lookup(np.array(['a', 'd', 'e', 'z'], 'O'))
Out[16]:
array([ 0,  3,  4, -1])

值不唯一的Index

Index中的每个值都唯一时,可以使用HashTable将值映射到其位置之上。若值不唯一,则Pandas会采用两种较慢的算法。

  • 值唯一时,采用HashTable映射,复杂度为O(1),但需要额外的内存保存HashTable
  • 值排序时使用二分搜索法,复杂度为O(log2(N))
  • 值无序时则逐个比较,复杂度为O(N)

在Pandas的内部实现中,还考虑了内存的因素,因此对于较大的、排序的、值唯一的Index,也会采用二分搜索法,省去了由HashTable带来的额外内存开销。

下面我们创建这三种Index对象:

In [17]:
N = 10000
unique_keys = np.array(list(set(pd.core.common.rands(5) for i in xrange(N))), 'O')
duplicate_keys = unique_keys.copy()
duplicate_keys[-1] = duplicate_keys[0]
sorted_keys = np.sort(duplicate_keys)

unique_index = pd.Index(unique_keys)
sorted_index = pd.Index(sorted_keys)
duplicate_index = pd.Index(duplicate_keys)

to_search = unique_keys[N-2]

每个Index都有is_uniqueis_monotonic属性,分别表示值是否唯一,和是否排序。下面的程序显示上面三个Index对象的属性:

In [18]:
from itertools import product

def dataframe_fromfunc(func, index, columns):
    return pd.DataFrame([[func(idx, col) for col in columns] for idx in index],
            index = index, columns = columns)

predicates = ["is_unique", "is_monotonic"]
index = ["unique_index", "sorted_index", "duplicate_index"]

dataframe_fromfunc(lambda idx, pred:getattr(globals()[idx], pred), index, predicates)
Out[18]:
is_unique is_monotonic
unique_index True False
sorted_index False True
duplicate_index False False

所有的Index都支持get_loc(),但只有值唯一的Index才支持get_indexer()。三者的get_loc()方法所返回的位置信息也不同:

  • unique_index:由于值是唯一的,因此返回一个整数
  • sorted_index:由于值是排序的,因此返回一个slice对象
  • duplicate_index:返回一个布尔数组

这三种返回值都可以作为下标存取ndarray中的值。

In [19]:
print unique_index.get_loc(unique_keys[0])
print sorted_index.get_loc(unique_keys[0])
print duplicate_index.get_loc(unique_keys[0])
0
slice(6574, 6576, None)
[ True False False ..., False False  True]

下面比较三者的运算速度:

In [20]:
%timeit unique_index.get_loc(to_search)
1000000 loops, best of 3: 846 ns per loop
In [21]:
%timeit sorted_index.get_loc(to_search)
100000 loops, best of 3: 15 us per loop
In [22]:
%timeit duplicate_index.get_loc(to_search)
1000 loops, best of 3: 271 us per loop

loading...