第36单元 数据转换
你现在已经准备好一睹pandas的“强劲套装”了,它们是矢量化的算术、逻辑运算以及其他数据转换机制。
算术运算
pandas支持四种算术运算(加法、减法、乘法和除法)和numpy的通用函数(ufunc,参考第25单元)。可以使用相应的运算符和函数来组合具有相同大小和结构的frame、frame列和series,以及相同大小的series。
有了算术运算,我们终于可以来更正alco frame的“Total”列了:
alco["Total"] = alco.Wine + alco.Spirits + alco.Beer alco.head() ➾ Beer Wine Spirits Total ➾ State Year ➾ Alabama 1977 0.99 0.13 0.84 1.96 ➾ 1978 0.98 0.12 0.88 1.98 ➾ 1979 0.98 0.12 0.84 1.94 ➾ 1980 0.96 0.16 0.74 1.86 ➾ 1981 1.00 0.19 0.73 1.92
如果要用对数尺度来表示总消耗量,可以使用numpy提供的log10()、log()和许多其他通用函数:
np.log10(alco.Total).head() ➾ State Year ➾ Alabama 1977 0.292256 ➾ 1978 0.296665 ➾ 1979 0.287802 ➾ 1980 0.269513 ➾ 1981 0.283301 ➾ Name: Total, dtype: float64
所有算术运算都保留索引。该功能称为数据对齐:两个series相加时,pandas会将一个series中带索引“C”的项添加到另一个series中的同名项目中。如果不存在同名项目,则相加的结果为nan。下面我们来模拟基因工程的做法,从第35单元第3小节的原始DNA片段中去除核苷酸C和T,得出两个DNA片段,然后对两个片段的不同核苷酸计数:
dna = "AGTCCGCGAATACAGGCTCGGT" dna1 = dna.replace("C", "") dna2 = dna.replace("T", "") dna_as_series1 = pd.Series(list(dna1), name="genes") # 移除所有C dna_as_series2 = pd.Series(list(dna2), name="genes") # 移除所有T dna_as_series1.value_counts() + dna_as_series2.value_counts() ➾ A 10 ➾ C NaN ➾ G 14 ➾ T NaN ➾ Name: genes, dtype: float64
在进行数据聚合之前,检查一下数据,看看是否需要进行缺失数据的清理(参考第33单元)。
数据聚合
数据聚合由数据分割、聚合和组合这三个步骤组成。
(1) 在分割步骤中,数据按单个键或多个键分割成块 。
(2) 在聚合函数的应用步骤中,将一个聚合函数(比如sum()或count())应用于每个数据块。
(3) 在组合步骤中,计算结果被合并为一个新的series或frame。
pandas的强大就在于它拥有groupby()函数和大批聚合函数,可以自动执行这里列出的三个步骤,所以我们要做的就是喝杯咖啡、坐享其成。
函数groupby()通过基于一个或多个分类的键值将行分到不同的组中,从而实现对frame的分割。该函数返回一个组生成器,可以在循环中使用(来访问组的内容)或者与聚合函数一起使用。
聚合函数包括count()(返回组中的行数),sum()(返回组中数字行的总和),mean()、median()、std()和var()(每个函数都返回组中所有行相应的统计量度量),min()和max()(返回组中最小和最大的行),prod()(返回组中数字行的乘积),first()和last()(返回组中的第一行和最后一行,该函数仅对有序的frame有意义)。
下面来考察一下我们一直在使用的alco frame,并得出一年中所有州总的酒消费量:
# 下面按年份(列“Year”)分组 alco_noidx = alco.reset_index() sum_alco = alco_noidx.groupby("Year").sum() sum_alco.tail() ➾ Beer Wine Spirits Total ➾ Year ➾ 2005 63.49 18.06 38.89 120.44 ➾ 2006 64.37 18.66 40.15 123.18 ➾ 2007 64.67 19.08 40.97 124.72 ➾ 2008 64.67 19.41 41.59 125.67 ➾ 2009 63.22 19.59 41.81 124.62
如果使用多个列进行分割,则结果具有多个索引,每个列对应一个级别的索引。
还可以在一个for循环中,通过组的迭代访问每个组的内容。在每次迭代中,groupby()函数返回的生成器提供索引条目以及与条目对应的行组(以frame的形式):
for year, year_frame in alco_noidx.groupby("Year"): «do_something(year, year_frame)»
有时你可能希望使用一个可计算的属性来实现行的分组,而不是使用现有的某个列或多个列。pandas允许使用字典或series的映射来实现数据聚合。让我们考虑一个将州映射到美国人口普查局定义的地区7的字典:
7www2.census.gov/geo/docs/maps-data/maps/reg_div.txt
state2reg ➾ {'Idaho': 'West', 'West Virginia': 'South', 'Vermont': 'Northeast', «...»}
现在可以按地区计算酒精的平均消费量了!请记住,字典的操作对象是行索引标签,而不是行的值,因此需要给某些列分配frame索引(至少在分组操作时需要这样处理)。字典的值(同时也是组的名称)成为返回的frame的索引:
alco2009.groupby(state2reg).mean() ➾ Beer Wine Spirits ➾ Midwest 1.324167 0.265000 0.822500 ➾ Northeast 1.167778 0.542222 0.904444 ➾ South 1.207500 0.275625 0.699375 ➾ West 1.249231 0.470769 0.843846
奥卡姆8是英国方济会的一位修道士、经院哲学家和神学家。用他的话说(其实并不是他说的),数据聚合精简了实体,因此对我们是有好处的。相反,离散化将值转换为类别,增加了实体,这对我们是不利的,除非它确实非常有用。
8en.wikipedia.org/wiki/William_of_Ockham
离散化
离散化是指将连续变量转换为离散(分类)变量,通常用于直方图和机器学习(参见第10章)。
函数cut()将作为第一个参数的数组或series分割成半开半闭的区间(即类别)。第二个参数是一个数(代表相同大小的区间的个数),或者是一个区间的边界列表。如果要将序列分割成N个区间,则可以传递N+1个区间边界组成的列表。由cut()函数生成的类别属于序数型数据:可以对它们进行排序和相互比较。
cats = pd.cut(alco2009['Wine'], 3).head() ➾ State ➾ Alabama (0.0991, 0.4] ➾ Alaska (0.4, 0.7] ➾ Arizona (0.0991, 0.4] ➾ Arkansas (0.0991, 0.4] ➾ California (0.4, 0.7] ➾ Name: Wine, dtype: category ➾ Categories (3, object): [(0.0991, 0.4] < (0.4, 0.7] < (0.7, 1]]
如果你想制作自己的类别标签,只需要传递另一个可选参数labels(N个标签组成的列表,每个区间对应一个标签)。
cats = pd.cut(alco2009['Wine'], 3, labels=("Low", "Moderate", "Heavy"')) cats.head() ➾ State ➾ Alabama Low ➾ Alaska Moderate ➾ Arizona Low ➾ Arkansas Low ➾ California Moderate ➾ Name: Wine, dtype: category ➾ Categories (3, object): [Low < Moderate < Heavy]
如果设置labels=False,则cut()函数只对数据区间进行编号而不做标记,并返回区间的成员信息:
cats = pd.cut(alco2009['Wine'], 3, labels=False).head() ➾ State ➾ Alabama 0 ➾ Alaska 1 ➾ Arizona 0 ➾ Arkansas 0 ➾ California 1 ➾ Name: Wine, dtype: int64
函数qcuts()与函数cuts()类似,不同的是qcuts()使用分位数而不是区间宽度进行分割。可以用它来计算分位数(比如中位数和四分位数)。
quants = pd.qcut(alco2009['Wine'], 3, labels=("Low", "Moderate", "Heavy")) quants.head() ➾ State ➾ Alabama Low ➾ Alaska Heavy ➾ Arizona Moderate ➾ Arkansas Low ➾ California Heavy ➾ Name: Wine, dtype: category ➾ Categories (3, object): [Low < Moderate < Heavy]
另一种使用少量可能值(这些值已分过类!)离散化变量的方法是将其分解为一组虚拟指标变量,每个变量对应一个可能的值。
虚拟变量是一个布尔型变量,与其对应的类别变量的值为true,其他所有值为false。在逻辑回归以及其他形式的机器学习中用到了虚拟变量,第10章将讨论相关内容。
函数get_dummies()将数组、series或frame转换为与原始对象拥有相同索引的另一个frame,每个列对应一个虚拟变量。如果对象是一个frame,请使用可选参数columns(需要离散化的列组成的列表)。
回顾在第36单元第2小节给出的州的地区划分,此处给出这种划分的使用指示器的表示法:
pd.get_dummies(state2reg).sort_index().head() Midwest Northeast South West ➾ state ➾ Alabama 0 0 1 0 ➾ Alaska 0 0 0 1 ➾ Arizona 0 0 0 1 ➾ Arkansas 0 0 1 0 ➾ California 0 0 0 1
因为每个州都只属于一个区域,所以每行中所有值的总和总是等于1,对于任何虚拟变量也是如此。
映射
映射是最一般的数据转换方式。它使用map()函数将任意的单参数函数应用于选中列中的每个元素。单参数函数可以是内置的Python函数、任意导入模块的函数、用户定义的函数或匿名的lambda函数。
举个例子,我们来创建三个字母的州名缩写。
with_state = alco2009.reset_index() abbrevs = with_state["State"].map(lambda x: x[:3].upper()) abbrevs.head() ➾ 0 ALA ➾ 1 ALA ➾ 2 ARI ➾ 3 ARK ➾ 4 CAL ➾ Name: State, dtype: object
显然,我们没能创建出唯一的三个字母的缩写,但为了理解map()函数,这个例子还是值得一试的!
与高度优化和并行化的通用函数不同,作为参数传递给map()的函数是由Python解释器执行的,无法对其进行优化。这使得map()函数非常低效,所以应该在没有其他选择时才使用它。
交叉表
交叉表计算组频率,并返回一个frame,其行和列分别对应两个分类变量(因子)的不同值。如果提供可选参数marginins=True,该函数还会计算行和列小计值。
以下代码计算一个州是“葡萄酒州”(葡萄酒消费量高于平均水平)还是“啤酒州”(啤酒消费量高于平均水平)的联合频率:
wine_state = alco2009["Wine"] > alco2009["Wine"].mean() beer_state = alco2009["Beer"] > alco2009["Beer"].mean() pd.crosstab(wine_state, beer_state) ➾ Beer False True ➾ Wine ➾ False 14 15 ➾ True 12 10
如果表中数字的差异不是很大(本例中的数字差异就不大!),那么这两个因子可能就是独立的。我们将在第47单元第2小节中回顾这个例子。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论