- 7.2 数据转换
- 移除重复数据
- 利用函数或映射进行数据转换
- 替换值
- 重命名轴索引
- 离散化和面元划分
- 检测和过滤异常值
- 排列和随机采样
- 计算指标/哑变量
7.2 数据转换
本章到目前为止介绍的都是数据的重排。另一类重要操作则是过滤、清理以及其他的转换工作。
移除重复数据
DataFrame中出现重复行有多种原因。下面就是一个例子:
In [45]: data = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'],....: 'k2': [1, 1, 2, 3, 3, 4, 4]})In [46]: dataOut[46]:k1 k20 one 11 two 12 one 23 two 34 one 35 two 46 two 4
DataFrame的duplicated方法返回一个布尔型Series,表示各行是否是重复行(前面出现过的行):
In [47]: data.duplicated()Out[47]:0 False1 False2 False3 False4 False5 False6 Truedtype: bool
还有一个与此相关的drop_duplicates方法,它会返回一个DataFrame,重复的数组会标为False:
In [48]: data.drop_duplicates()Out[48]:k1 k20 one 11 two 12 one 23 two 34 one 35 two 4
这两个方法默认会判断全部列,你也可以指定部分列进行重复项判断。假设我们还有一列值,且只希望根据k1列过滤重复项:
In [49]: data['v1'] = range(7)In [50]: data.drop_duplicates(['k1'])Out[50]:k1 k2 v10 one 1 01 two 1 1
duplicated和drop_duplicates默认保留的是第一个出现的值组合。传入keep=’last’则保留最后一个:
In [51]: data.drop_duplicates(['k1', 'k2'], keep='last')Out[51]:k1 k2 v10 one 1 01 two 1 12 one 2 23 two 3 34 one 3 46 two 4 6
利用函数或映射进行数据转换
对于许多数据集,你可能希望根据数组、Series或DataFrame列中的值来实现转换工作。我们来看看下面这组有关肉类的数据:
In [52]: data = pd.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]})In [53]: dataOut[53]:food ounces0 bacon 4.01 pulled pork 3.02 bacon 12.03 Pastrami 6.04 corned beef 7.55 Bacon 8.06 pastrami 3.07 honey ham 5.08 nova lox 6.0
假设你想要添加一列表示该肉类食物来源的动物类型。我们先编写一个不同肉类到动物的映射:
meat_to_animal = {'bacon': 'pig','pulled pork': 'pig','pastrami': 'cow','corned beef': 'cow','honey ham': 'pig','nova lox': 'salmon'}
Series的map方法可以接受一个函数或含有映射关系的字典型对象,但是这里有一个小问题,即有些肉类的首字母大写了,而另一些则没有。因此,我们还需要使用Series的str.lower方法,将各个值转换为小写:
In [55]: lowercased = data['food'].str.lower()In [56]: lowercasedOut[56]:0 bacon1 pulled pork2 bacon3 pastrami4 corned beef5 bacon6 pastrami7 honey ham8 nova loxName: food, dtype: objectIn [57]: data['animal'] = lowercased.map(meat_to_animal)In [58]: dataOut[58]:food ounces animal0 bacon 4.0 pig1 pulled pork 3.0 pig2 bacon 12.0 pig3 Pastrami 6.0 cow4 corned beef 7.5 cow5 Bacon 8.0 pig6 pastrami 3.0 cow7 honey ham 5.0 pig8 nova lox 6.0 salmon
我们也可以传入一个能够完成全部这些工作的函数:
In [59]: data['food'].map(lambda x: meat_to_animal[x.lower()])Out[59]:0 pig1 pig2 pig3 cow4 cow5 pig6 cow7 pig8 salmonName: food, dtype: object
使用map是一种实现元素级转换以及其他数据清理工作的便捷方式。
替换值
利用fillna方法填充缺失数据可以看做值替换的一种特殊情况。前面已经看到,map可用于修改对象的数据子集,而replace则提供了一种实现该功能的更简单、更灵活的方式。我们来看看下面这个Series:
In [60]: data = pd.Series([1., -999., 2., -999., -1000., 3.])In [61]: dataOut[61]:0 1.01 -999.02 2.03 -999.04 -1000.05 3.0
-999这个值可能是一个表示缺失数据的标记值。要将其替换为pandas能够理解的NA值,我们可以利用replace来产生一个新的Series(除非传入inplace=True):
In [62]: data.replace(-999, np.nan)Out[62]:0 1.01 NaN2 2.03 NaN4 -1000.05 3.0dtype: float64
如果你希望一次性替换多个值,可以传入一个由待替换值组成的列表以及一个替换值::
In [63]: data.replace([-999, -1000], np.nan)Out[63]:0 1.01 NaN2 2.03 NaN4 NaN5 3.0dtype: float64
要让每个值有不同的替换值,可以传递一个替换列表:
In [64]: data.replace([-999, -1000], [np.nan, 0])Out[64]:0 1.01 NaN2 2.03 NaN4 0.05 3.0dtype: float64
传入的参数也可以是字典:
In [65]: data.replace({-999: np.nan, -1000: 0})Out[65]:0 1.01 NaN2 2.03 NaN4 0.05 3.0dtype: float64
笔记:data.replace方法与data.str.replace不同,后者做的是字符串的元素级替换。我们会在后面学习Series的字符串方法。
重命名轴索引
跟Series中的值一样,轴标签也可以通过函数或映射进行转换,从而得到一个新的不同标签的对象。轴还可以被就地修改,而无需新建一个数据结构。接下来看看下面这个简单的例子:
In [66]: data = pd.DataFrame(np.arange(12).reshape((3, 4)),....: index=['Ohio', 'Colorado', 'New York'],....: columns=['one', 'two', 'three', 'four'])
跟Series一样,轴索引也有一个map方法:
In [67]: transform = lambda x: x[:4].upper()In [68]: data.index.map(transform)Out[68]: Index(['OHIO', 'COLO', 'NEW '], dtype='object')
你可以将其赋值给index,这样就可以对DataFrame进行就地修改:
In [69]: data.index = data.index.map(transform)In [70]: dataOut[70]:one two three fourOHIO 0 1 2 3COLO 4 5 6 7NEW 8 9 10 11
如果想要创建数据集的转换版(而不是修改原始数据),比较实用的方法是rename:
In [71]: data.rename(index=str.title, columns=str.upper)Out[71]:ONE TWO THREE FOUROhio 0 1 2 3Colo 4 5 6 7New 8 9 10 11
特别说明一下,rename可以结合字典型对象实现对部分轴标签的更新:
In [72]: data.rename(index={'OHIO': 'INDIANA'},....: columns={'three': 'peekaboo'})Out[72]:one two peekaboo fourINDIANA 0 1 2 3COLO 4 5 6 7NEW 8 9 10 11
rename可以实现复制DataFrame并对其索引和列标签进行赋值。如果希望就地修改某个数据集,传入inplace=True即可:
In [73]: data.rename(index={'OHIO': 'INDIANA'}, inplace=True)In [74]: dataOut[74]:one two three fourINDIANA 0 1 2 3COLO 4 5 6 7NEW 8 9 10 11
离散化和面元划分
为了便于分析,连续数据常常被离散化或拆分为“面元”(bin)。假设有一组人员数据,而你希望将它们划分为不同的年龄组:
In [75]: ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
接下来将这些数据划分为“18到25”、“26到35”、“35到60”以及“60以上”几个面元。要实现该功能,你需要使用pandas的cut函数:
In [76]: bins = [18, 25, 35, 60, 100]In [77]: cats = pd.cut(ages, bins)In [78]: catsOut[78]:[(18, 25], (18, 25], (18, 25], (25, 35], (18, 25], ..., (25, 35], (60, 100], (35,60], (35, 60], (25, 35]]Length: 12Categories (4, interval[int64]): [(18, 25] < (25, 35] < (35, 60] < (60, 100]]
pandas返回的是一个特殊的Categorical对象。结果展示了pandas.cut划分的面元。你可以将其看做一组表示面元名称的字符串。它的底层含有一个表示不同分类名称的类型数组,以及一个codes属性中的年龄数据的标签:
In [79]: cats.codesOut[79]: array([0, 0, 0, 1, 0, 0, 2, 1, 3, 2, 2, 1], dtype=int8)In [80]: cats.categoriesOut[80]:IntervalIndex([(18, 25], (25, 35], (35, 60], (60, 100]]closed='right',dtype='interval[int64]')In [81]: pd.value_counts(cats)Out[81]:(18, 25] 5(35, 60] 3(25, 35] 3(60, 100] 1dtype: int64
pd.value_counts(cats)是pandas.cut结果的面元计数。
跟“区间”的数学符号一样,圆括号表示开端,而方括号则表示闭端(包括)。哪边是闭端可以通过right=False进行修改:
In [82]: pd.cut(ages, [18, 26, 36, 61, 100], right=False)Out[82]:[[18, 26), [18, 26), [18, 26), [26, 36), [18, 26), ..., [26, 36), [61, 100), [36,61), [36, 61), [26, 36)]Length: 12Categories (4, interval[int64]): [[18, 26) < [26, 36) < [36, 61) < [61, 100)]
你可 以通过传递一个列表或数组到labels,设置自己的面元名称:
In [83]: group_names = ['Youth', 'YoungAdult', 'MiddleAged', 'Senior']In [84]: pd.cut(ages, bins, labels=group_names)Out[84]:[Youth, Youth, Youth, YoungAdult, Youth, ..., YoungAdult, Senior, MiddleAged, MiddleAged, YoungAdult]Length: 12Categories (4, object): [Youth < YoungAdult < MiddleAged < Senior]
如果向cut传入的是面元的数量而不是确切的面元边界,则它会根据数据的最小值和最大值计算等长面元。下面这个例子中,我们将一些均匀分布的数据分成四组:
In [85]: data = np.random.rand(20)In [86]: pd.cut(data, 4, precision=2)Out[86]:[(0.34, 0.55], (0.34, 0.55], (0.76, 0.97], (0.76, 0.97], (0.34, 0.55], ..., (0.34, 0.55], (0.34, 0.55], (0.55, 0.76], (0.34, 0.55], (0.12, 0.34]]Length: 20Categories (4, interval[float64]): [(0.12, 0.34] < (0.34, 0.55] < (0.55, 0.76] <(0.76, 0.97]]
选项precision=2,限定小数只有两位。
qcut是一个非常类似于cut的函数,它可以根据样本分位数对数据进行面元划分。根据数据的分布情况,cut可能无法使各个面元中含有相同数量的数据点。而qcut由于使用的是样本分位数,因此可以得到大小基本相等的面元:
In [87]: data = np.random.randn(1000) # Normally distributedIn [88]: cats = pd.qcut(data, 4) # Cut into quartilesIn [89]: catsOut[89]:[(-0.0265, 0.62], (0.62, 3.928], (-0.68, -0.0265], (0.62, 3.928], (-0.0265, 0.62], ..., (-0.68, -0.0265], (-0.68, -0.0265], (-2.95, -0.68], (0.62, 3.928], (-0.68,-0.0265]]Length: 1000Categories (4, interval[float64]): [(-2.95, -0.68] < (-0.68, -0.0265] < (-0.0265,0.62] <(0.62, 3.928]]In [90]: pd.value_counts(cats)Out[90]:(0.62, 3.928] 250(-0.0265, 0.62] 250(-0.68, -0.0265] 250(-2.95, -0.68] 250dtype: int64
与cut类似,你也可以传递自定义的分位数(0到1之间的数值,包含端点):
In [91]: pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.])Out[91]:[(-0.0265, 1.286], (-0.0265, 1.286], (-1.187, -0.0265], (-0.0265, 1.286], (-0.0265, 1.286], ..., (-1.187, -0.0265], (-1.187, -0.0265], (-2.95, -1.187], (-0.0265,1.286], (-1.187, -0.0265]]Length: 1000Categories (4, interval[float64]): [(-2.95, -1.187] < (-1.187, -0.0265] < (-0.0265, 1.286] <(1.286, 3.928]]
本章稍后在讲解聚合和分组运算时会再次用到cut和qcut,因为这两个离散化函数对分位和分组分析非常重要。
检测和过滤异常值
过滤或变换异常值(outlier)在很大程度上就是运用数组运算。来看一个含有正态分布数据的DataFrame:
In [92]: data = pd.DataFrame(np.random.randn(1000, 4))In [93]: data.describe()Out[93]:0 1 2 3count 1000.000000 1000.000000 1000.000000 1000.000000mean 0.049091 0.026112 -0.002544 -0.051827std 0.996947 1.007458 0.995232 0.998311min -3.645860 -3.184377 -3.745356 -3.42825425% -0.599807 -0.612162 -0.687373 -0.74747850% 0.047101 -0.013609 -0.022158 -0.08827475% 0.756646 0.695298 0.699046 0.623331max 2.653656 3.525865 2.735527 3.366626
假设你想要找出某列中绝对值大小超过3的值:
In [94]: col = data[2]In [95]: col[np.abs(col) > 3]Out[95]:41 -3.399312136 -3.745356Name: 2, dtype: float64
要选出全部含有“超过3或-3的值”的行,你可以在布尔型DataFrame中使用any方法:
In [96]: data[(np.abs(data) > 3).any(1)]Out[96]:0 1 2 341 0.457246 -0.025907 -3.399312 -0.97465760 1.951312 3.260383 0.963301 1.201206136 0.508391 -0.196713 -3.745356 -1.520113235 -0.242459 -3.056990 1.918403 -0.578828258 0.682841 0.326045 0.425384 -3.428254322 1.179227 -3.184377 1.369891 -1.074833544 -3.548824 1.553205 -2.186301 1.277104635 -0.578093 0.193299 1.397822 3.366626782 -0.207434 3.525865 0.283070 0.544635803 -3.645860 0.255475 -0.549574 -1.907459
根据这些条件,就可以对值进行设置。下面的代码可以将值限制在区间-3到3以内:
In [97]: data[np.abs(data) > 3] = np.sign(data) * 3In [98]: data.describe()Out[98]:0 1 2 3count 1000.000000 1000.000000 1000.000000 1000.000000mean 0.050286 0.025567 -0.001399 -0.051765std 0.992920 1.004214 0.991414 0.995761min -3.000000 -3.000000 -3.000000 -3.00000025% -0.599807 -0.612162 -0.687373 -0.74747850% 0.047101 -0.013609 -0.022158 -0.08827475% 0.756646 0.695298 0.699046 0.623331max 2.653656 3.000000 2.735527 3.000000
根据数据的值是正还是负,np.sign(data)可以生成1和-1:
In [99]: np.sign(data).head()Out[99]:0 1 2 30 -1.0 1.0 -1.0 1.01 1.0 -1.0 1.0 -1.02 1.0 1.0 1.0 -1.03 -1.0 -1.0 1.0 -1.04 -1.0 1.0 -1.0 -1.0
排列和随机采样
利用numpy.random.permutation函数可以轻松实现对Series或DataFrame的列的排列工作(permuting,随机重排序)。通过需要排列的轴的长度调用permutation,可产生一个表示新顺序的整数数组:
In [100]: df = pd.DataFrame(np.arange(5 * 4).reshape((5, 4)))In [101]: sampler = np.random.permutation(5)In [102]: samplerOut[102]: array([3, 1, 4, 2, 0])
然后就可以在基于iloc的索引操作或take函数中使用该数组了:
In [103]: dfOut[103]:0 1 2 30 0 1 2 31 4 5 6 72 8 9 10 113 12 13 14 154 16 17 18 19In [104]: df.take(sampler)Out[104]:0 1 2 33 12 13 14 151 4 5 6 74 16 17 18 192 8 9 10 110 0 1 2 3
如果不想用替换的方式选取随机子集,可以在Series和DataFrame上使用sample方法:
In [105]: df.sample(n=3)Out[105]:0 1 2 33 12 13 14 154 16 17 18 192 8 9 10 11
要通过替换的方式产生样本(允许重复选择),可以传递replace=True到sample:
In [106]: choices = pd.Series([5, 7, -1, 6, 4])In [107]: draws = choices.sample(n=10, replace=True)In [108]: drawsOut[108]:4 41 74 42 -10 53 61 74 40 54 4dtype: int64
计算指标/哑变量
另一种常用于统计建模或机器学习的转换方式是:将分类变量(categorical variable)转换为“哑变量”或“指标矩阵”。
如果DataFrame的某一列中含有k个不同的值,则可以派生出一个k列矩阵或DataFrame(其值全为1和0)。pandas有一个get_dummies函数可以实现该功能(其实自己动手做一个也不难)。使用之前的一个DataFrame例子:
In [109]: df = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'],.....: 'data1': range(6)})In [110]: pd.get_dummies(df['key'])Out[110]:a b c0 0 1 01 0 1 02 1 0 03 0 0 14 1 0 05 0 1 0
有时候,你可能想给指标DataFrame的列加上一个前缀,以便能够跟其他数据进行合并。get_dummies的prefix参数可以实现该功能:
In [111]: dummies = pd.get_dummies(df['key'], prefix='key')In [112]: df_with_dummy = df[['data1']].join(dummies)In [113]: df_with_dummyOut[113]:data1 key_a key_b key_c0 0 0 1 01 1 0 1 02 2 1 0 03 3 0 0 14 4 1 0 05 5 0 1 0
如果DataFrame中的某行同属于多个分类,则事情就会有点复杂。看一下MovieLens 1M数据集,14章会更深入地研究它:
In [114]: mnames = ['movie_id', 'title', 'genres']In [115]: movies = pd.read_table('datasets/movielens/movies.dat', sep='::',.....: header=None, names=mnames)In [116]: movies[:10]Out[116]:movie_id title genres0 1 Toy Story (1995) Animation|Children's|Comedy1 2 Jumanji (1995) Adventure|Children's|Fantasy2 3 Grumpier Old Men (1995) Comedy|Romance3 4 Waiting to Exhale (1995) Comedy|Drama4 5 Father of the Bride Part II (1995) Comedy5 6 Heat (1995) Action|Crime|Thriller6 7 Sabrina (1995) Comedy|Romance7 8 Tom and Huck (1995) Adventure|Children's8 9 Sudden Death (1995)Action9 10 GoldenEye (1995) Action|Adventure|Thriller
要为每个genre添加指标变量就需要做一些数据规整操作。首先,我们从数据集中抽取出不同的genre值:
In [117]: all_genres = []In [118]: for x in movies.genres:.....: all_genres.extend(x.split('|'))In [119]: genres = pd.unique(all_genres)
现在有:
In [120]: genresOut[120]:array(['Animation', "Children's", 'Comedy', 'Adventure', 'Fantasy','Romance', 'Drama', 'Action', 'Crime', 'Thriller','Horror','Sci-Fi', 'Documentary', 'War', 'Musical', 'Mystery', 'Film-Noir','Western'], dtype=object)
构建指标DataFrame的方法之一是从一个全零DataFrame开始:
In [121]: zero_matrix = np.zeros((len(movies), len(genres)))In [122]: dummies = pd.DataFrame(zero_matrix, columns=genres)
现在,迭代每一部电影,并将dummies各行的条目设为1。要这么做,我们使用dummies.columns来计算每个类型的列索引:
In [123]: gen = movies.genres[0]In [124]: gen.split('|')Out[124]: ['Animation', "Children's", 'Comedy']In [125]: dummies.columns.get_indexer(gen.split('|'))Out[125]: array([0, 1, 2])
然后,根据索引,使用.iloc设定值:
In [126]: for i, gen in enumerate(movies.genres):.....: indices = dummies.columns.get_indexer(gen.split('|')).....: dummies.iloc[i, indices] = 1.....:
然后,和以前一样,再将其与movies合并起来:
In [127]: movies_windic = movies.join(dummies.add_prefix('Genre_'))In [128]: movies_windic.iloc[0]Out[128]:movie_id 1title Toy Story (1995)genres Animation|Children's|ComedyGenre_Animation 1Genre_Children's 1Genre_Comedy 1Genre_Adventure 0Genre_Fantasy 0Genre_Romance 0Genre_Drama 0...Genre_Crime 0Genre_Thriller 0Genre_Horror 0Genre_Sci-Fi 0Genre_Documentary 0Genre_War 0Genre_Musical 0Genre_Mystery 0Genre_Film-Noir 0Genre_Western 0Name: 0, Length: 21, dtype: object
笔记:对于很大的数据,用这种方式构建多成员指标变量就会变得非常慢。最好使用更低级的函数,将其写入NumPy数组,然后结果包装在DataFrame中。
一个对统计应用有用的秘诀是:结合get_dummies和诸如cut之类的离散化函数:
In [129]: np.random.seed(12345)In [130]: values = np.random.rand(10)In [131]: valuesOut[131]:array([ 0.9296, 0.3164, 0.1839, 0.2046, 0.5677, 0.5955, 0.9645,0.6532, 0.7489, 0.6536])In [132]: bins = [0, 0.2, 0.4, 0.6, 0.8, 1]In [133]: pd.get_dummies(pd.cut(values, bins))Out[133]:(0.0, 0.2] (0.2, 0.4] (0.4, 0.6] (0.6, 0.8] (0.8, 1.0]0 0 0 0 0 11 0 1 0 0 02 1 0 0 0 03 0 1 0 0 04 0 0 1 0 05 0 0 1 0 06 0 0 0 0 17 0 0 0 1 08 0 0 0 1 09 0 0 0 1 0
我们用numpy.random.seed,使这个例子具有确定性。本书后面会介绍pandas.get_dummies。
