7.2 重塑和轴向旋转
重塑层次化索引
stack
:将数据的列旋转为行unstack
:将数据的行旋转为列
1 | data = DataFrame(np.arange(6).reshape((2, 3)), |
Data 1:
1 | number one two three |
使用stack
方法可以将列转换为行,得到一个Series:
1 | result = data.stack() |
Output:
1 | state number |
对于一个层次化索引的Series,看可以用unstack
将其重排为一个DataFrame:
1 | result.unstack() |
Output like Data 1
默认情况下,unstack
和stack
操作的是最内层索引,但可以传入分层级别的编号或名称对其他级别进行重塑。
1 | result.unstack(0) |
Output:
1 | state Ohio Colorado |
如果在重塑时,不是所有的分组都有值,则unstack
操作会引入缺失值,而stack
操作会滤除缺失值,因此这两个操作互相可逆。在DataFrame进行unstack
操作时,作为旋转轴的级别会成为结果中最低级别。
将“长格式”旋转为“宽格式”
一般时间序列数据是以“长格式”或“堆叠格式”存储的,时间作为主键或主键之一,记录数据的关系型数据大多这样保存原始的时间序列数据。
1 | ldata[:10] |
Data:
1 | date item value |
在DataFrame中,你可能更喜欢这样操作数据:不同的item值分别形成一列,data列中的时间值则用做索引。DataFrame的pivot
方法可以实现这个转换。
1 | pivoted = ldata.pivot('date', 'item', 'value') |
Output:
1 | item infl realgdp unemp |
pivot
的前两个参数值分别用作行和列索引的列名,最后一个参数值则是用于填充DataFrame的数据列的列名。如果有两个需要参与重塑的数据列,忽略最后一个参数得到的DataFrame就会带有层次化的列。
1 | ldata['value2'] = np.random.randn(len(ldata)) |
Data:
1 | date item value value2 |
本质上pivot
等同于:用set_index
创建层次化索引后再使用unstack
重塑。下面两段代码的效果相同。
1 | pivoted = ldata.pivot('date', 'item') |
Output:
1 | value value2 |
7.3 数据转换
移除重复数据
DataFrame中常常会出现重复行,DataFrame的duplicated
返回一个布尔型Series,表示各行是否重复,DataFrame的dorp_duplicates
返回一个移除重复行的DataFrame。默认情况下判断所有列,但是也可以传入希望过滤的列列表。默认情况下保留的是第一个出现的值组合,传入take_last=True
参数则保留最后一个。
1 | data = DataFrame({'k1': ['one'] * 3 + ['two'] * 4, |
Output:
1 | k1 k2 v1 |
利用函数或映射进行数据转换
更多的时候在处理数据时,我们希望根据某种逻辑关系对值进行转换。在说明前先看下如下关于肉类的演示数据:
1 | data = DataFrame({'food': ['bacon', 'pulled pork', 'bacon', 'Pastrami', 'corned beef', 'Bacon', 'pastrami', 'honey ham', 'nova lox'], 'ounces': [4, 3, 12, 6, 7.5, 8, 3, 5, 6]}) |
Data:
1 | food ounces |
最常见的处理是创建新分类/分组,例如需要添加一列表示food来源的动物类型,肉类到动物的映射关系如下:
1 | meat_to_animal = { |
Series的map
方法可以根据一个函数或含有映射关系的字典对象,产生新的Series。
1 | data['animal'] = data['food'].map(str.lower).map(meat_to_animal) |
Output:
1 | food ounces animal |
注意因为原始数据里包含有大小写,因此首先做了一次小写转换。当然也可以直接传入一个完成全部工作的lambda函数,效果是一样的:
1 | data['food'].map(lambda x: meat_to_animal[x.lower()]) |
值替换
fillna
方法是一种值替换的特殊情况,replace
方法是更加通用、更加简单的方法。
1 | data = Series([1., -999., 2., -999., -1000., 3.]) |
-999可能是一个表示缺失的标记值,可以用replace
方法替换为pandas的nan
:
1 | data.replace(-999, np.nan) |
如果希望一次性替换多个值,可以传入一个由待替换值和替换值组成的列表或字典,下面两种方式等效:
1 | data.replace([-999, -1000], [np.nan, 0])) |
重命名轴索引
map
方法同样可以对数据的轴标签使用。DataFrame的rename
方法可以直接返回修改index和columns的对象,如果希望就地修改不产生新对象,可以传入参数inplace=True
。
离散化和面元划分
为了便于分析,连续数据常常被离散化或分bin,例如将年龄划分为不同的年龄组。
Data:
1 | ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32] |
如果要将这些数据分为“18到25”、“26到35”、“35到60”和“60以上”几个组,可以使用pandas的cut
函数:
1 | bins = [18, 25, 35, 60, 100] |
Output:
1 | [(18, 25], (18, 25], (18, 25], (25, 35], (18, 25], ..., (25, 35], (60, 100], (35, 60], (35, 60], (25, 35]] |
cut
函数返回的是一个特殊的Categorical对象。它含有一个表示不同分类名称的categories
索引对象和一个为分组进行标号的levels
数组。
1 | cats.labels |
1 | cats.categories |
categories是一个Index对象,实质是一个由python对象组成的 NumPy数组,其中存放的是每个分组的描述字符串。
如果想cut
传入的是分组的数量而不是确切的分组边界,则会根据数据的最小值和最大值计算等长分组。qcut
是个类似于cut
的函数,它根据样本的分位数对数据进行分组划分。
1 | data = np.random.randn(1000) # 正态分布 |
Output:
1 | [(-0.022, 0.634], [-3.745, -0.648], (0.634, 3.26], (-0.022, 0.634], (-0.648, -0.022], ..., (0.634, 3.26], (-0.022, 0.634], [-3.745, -0.648], (-0.022, 0.634], (-0.022, 0.634]] |
跟cut
一样,可以设置自定义的分位数(0到1之间的数值,包含端点)。
1 | pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.]) |
Output:
1 | [(-0.022, 1.298], [-3.745, -1.274], (-0.022, 1.298], (-0.022, 1.298], (-1.274, -0.022], ..., (-0.022, 1.298], (-0.022, 1.298], [-3.745, -1.274], (-0.022, 1.298], (-0.022, 1.298]] |
检测和过滤异常值
异常值(这里指异常点或离群值)的过滤或变换本质上就是数组的运算,利用布尔型DataFrame方法可以选出异常值,然后就可以轻松设置。
1 | np.random.seed(12345) |
No output.
排列和随机采样
利用numpy.random.permutation函数可以实现对Series或DataFrame的列重排,传入需要排列的轴长度值,可产生一个表示新顺序的整数数组。
1 | df = DataFrame(np.arange(5 * 4).reshape(5, 4)) |
Output:
1 | array([1, 0, 2, 3, 4]) |
然后就可以在基于ix的索引操作或take
函数中使用该数组实现对DataFrame的重排序。
1 | df.take(sampler) |
Output:
1 | 0 1 2 3 |
如果希望进行重复采样,可以通过np.random.randint
得到一组随机整数。
1 | sampler = np.random.randint(0, len(df), size=10) # 随机抽取10条 |
Oupput:
1 | 0 1 2 3 |
计算指标/哑变量
另一种常用于统计建模或机器学习的转换方式是将分类变量转换为“哑变量矩阵”或“指标矩阵”。例如某一个列中含有k个不同的取值,则可以派生出一个k列矩阵,每列的值都是1或0。pandas有一个get_dummies
函数可以直接实现该功能。
1 | df = DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'], 'data1': range(6)}) |
Output:
1 | a b c |
如果DataFrame中某分类变量列的值属于多个分类,就需要做一些数据规整操作。
1 | mnames = ['movie_id', 'title', 'genres'] |
Data:
1 | movie_id title genres |
以之前MovieLens 1M数据为例,首先需要从genres
列中抽取出不同的genre值g。
1 | genre_iter = (set(x.split('|')) for x in movies.genres) |
genre_iter是一个表达式生成器,set.union
参数中,*genre_iter
表示将生成器的每次迭代都作为一个set对象,最后传给union
方法获得并集。Output:
1 | ['Action', |
为了构建指标,我们先从一个全零的DataFrame对象dummies
开始,迭代每一部电影并将dummies
各行的项设置为1或0,最后再将其与movies合并。
1 | dummies = DataFrame(np.zeros((len(movies), len(genres))), columns=genres) |
Output:
1 | movie_id 1 |
一个实用的秘诀是将get_dummies
和诸如cut
的离散化函数生成你需要的哑变量指标。