数据科学家的Pandas练习 - 第一部分
一组具有挑战性的Pandas问题
照片:Olav Ahrens Røtne on Unsplash
Pandas库一直吸引着数据科学家们用它来做一些惊人的事情。毫无疑问,它是处理、操作和处理表格数据的首选工具。
因此,为了扩展你的专业知识,挑战你现有的知识,并向你介绍数据科学家中众多流行的Pandas函数,我将介绍Pandas练习的第一部分。其目的是加强你的逻辑能力,并帮助你用最好的Python数据分析包之一来内化数据操作。
在这里可以找到包含这个测验的所有问题的笔记本。GitHub。
目录
- 基于另一个列表对DataFrame进行排序
- 在DataFrame的特定位置插入一个列
- 根据列的数据类型选择列
- 计算每一列的Non-NaN单元格的数量
- 将DataFrame分成相等的部分
- 将DataFrame逐行或逐列倒置
- 重新排列数据框架的列
- 获取一个数据框架的备用行
- 在任意位置插入一个行
- 对DataFrame的每个单元格应用函数
作为一个练习,我建议你先自己尝试一下这些问题,然后再看看我提供的解决方案。
请注意,我在这里提供的解决方案不一定是解决问题的唯一方法。你可能会想出一些不同的办法,但仍然是正确的。然而,如果发生这种情况,请发表评论,我很想知道你的方法。
让我们开始吧!
- 基于另一个列表对DataFrame进行排序
提示你有一个DataFrame。此外,你还有一个列表,其中包含DataFrame中某一列的所有唯一值。对DataFrame进行排序,使该列的值在给定的列表中以相同的顺序出现。
输入和预期的输出
import pandas as pd
df = pd.DataFrame([["A", 1], ["B", 2], ["C", 3], ["D", 4]],
columns = ["Col_A", "Col_B"])
sort_list = ["C", "D", "B", "A"]
output_df = # YOUR CODE HERE
"""
col_A col_B
0 C 3
1 D 4
2 B 2
3 A 1
"""
解决方案
这里的想法是要从给定的列表中生成一个系列。每个索引将表示字符,而相应的值将表示位置。利用这一点,我们可以将原始的DataFrame映射到生成的系列,并将其传递给sort_values()方法作为参考,如下图所示。
import pandas as pd
# Solution
s = pd.Series(range(len(sort_list)), index = sort_list)
df.sort_values("col_A", key = lambda x: x.map(s))
"""
col_A col_B
0 C 3
1 D 4
2 B 2
3 A 1
"""
P.S. 我们也可以用合并来解决这个问题。如果你能想出这个办法,请在评论中告诉我。
- 在DataFrame的特定位置插入一个列
提示假设你也有一个类似于上面使用的DataFrame。此外,你还得到一个列表,其大小与给定的DataFrame中的行数相同。任务是将给定的列表作为一个新的列插入到DataFrame的给定位置。
输入和预期的输出。
import pandas as pd
df = pd.DataFrame([["A", 1], ["B", 2], ["C", 3], ["D", 4]],
columns = ["Col_A", "Col_B"])
new_column = ["P", "Q", "R", "S"]
insert_position = 1
output_df = # YOUR CODE HERE
"""
col_A col_C col_B
0 A P 1
1 B Q 2
2 C R 3
3 D S 4
"""
解决方案
在这里,我们可以使用insert()方法,并将位置、column_name和值作为参数传递,如下所示。
import pandas as pd
df = pd.DataFrame([["A", 1], ["B", 2], ["C", 3], ["D", 4]],
columns = ["Col_A", "Col_B"])
new_column = ["P", "Q", "R", "S"]
insert_position = 1
output_df = # YOUR CODE HERE
"""
col_A col_C col_B
0 A P 1
1 B Q 2
2 C R 3
3 D S 4
"""
- 根据列的数据类型来选择列
提示。我们都熟悉基于行的过滤,不是吗?好吧,让我们试试别的方法。你的任务是过滤一个DataFrame中所有符合给定数据类型的列。
输入和预期的输出。
import pandas as pd
df = pd.DataFrame([["A", 1, True], ["B", 2, False],
["C", 3, False], ["D", 4, True]],
columns=["col_A", "col_B", "col_C"])
dt_type = "bool"
output_df = ## YOUR CODE HERE
"""
col_C
0 True
1 False
2 False
3 True
"""
解决方案
在这里,我们可以使用select_dtypes()方法并传递我们需要过滤掉的数据类型,如下图所示。
import pandas as pd
...
output_df = df.select_dtypes(include = dt_type)
"""
col_C
0 True
1 False
2 False
3 True
"""
- 计算每一列的Non-NaN单元格的数量
提示。接下来,给定一个DataFrame(在一个或多个列中有NaN值),你需要打印每一列的Non-NaN单元格的数量。
输入和预期的输出。
import pandas as pd
import numpy as np
df = pd.DataFrame([["A", np.NaN], [np.NaN, 2],
["C", np.NaN], ["D", 4]],
columns=["col_A", "col_B"])
"""
col_A col_B
0 A NaN
1 NaN 2.0
2 C NaN
3 D 4.0
"""
output_df = # YOUR CODE HERE
"""
col_A 3
col_B 2
"""
解决方案。
这里,我们可以使用count()方法来获得结果。如下图所示。
df.count()
"""
col_A 3
col_B 2
"""
- 将DataFrame分割成相等的部分
提示。给定一个DataFrame,你的任务是将DataFrame分割成给定数量的相等部分。
输入和预期的输出。
import pandas as pd
import numpy as np
df = pd.DataFrame([["A", 1], ["B", 2], ["C", 3], ["D", 4]],
columns=["col_A", "col_B"])
parts = 2
out_df1, out_df2 = ## YOUR CODE HERE
out_df1
"""
col_A col_B
0 A 1
1 B 2
"""
解决方案。
在这里,我们将使用NumPy的split()方法,并将部分的数量作为参数传递,如下所示。
mport pandas as pd
import numpy as np
out_df1, out_df2 = np.split(df, parts)
out_df1
"""
col_A col_B
0 A 1
1 B 2
"""
- 将DataFrame按行或按列反转
提示。接下来,考虑你有一个类似于我们上面使用的DataFrame。你的任务是将整个DataFrame按行或按列翻转。
输入和预期的输出。
import pandas as pd
df = pd.DataFrame([["A", 1], ["B", 2], ["C", 3], ["D", 4]],
columns=["col_A", "col_B"])
col_reverse = # YOUR CODE HERE
"""
col_B col_A
0 1 A
1 2 B
2 3 C
3 4 D
"""
解决方案。
我们可以使用loc(或iloc)并使用"::-1 "指定反向索引方法,如下图所示。
import pandas as pd
col_reverse = df.loc[:, ::-1]
row_reverse = df.loc[::-1]
- 重新排列一个数据框架的列
提示在这个练习中,你会得到一个DataFrame。另外,你有一个列表,指定了列在DataFrame中出现的顺序。给出列表和DataFrame,按照列表中指定的顺序打印各列。
输入和预期的输出。
import pandas as pd
df = pd.DataFrame([["A", 1], ["B", 2],
["C", 3], ["D", 4]],
columns=["col_A", "col_B"])
rearrange_order = [1,0] # Column 1 then Column 0
output_df = # YOUR CODE HERE
"""
col_B col_A
0 1 A
1 2 B
2 3 C
3 4 D
"""
解决方案。
与上面类似,我们可以使用iloc来选择所有的行,并指定列表中的列的顺序,如下图所示。
import pandas as pd
output_df = df.iloc[:, rearrange_order]
"""
col_B col_A
0 1 A
1 2 B
2 3 C
3 4 D
"""
- 获取DataFrame的备用行
提示接下来,给定一个DataFrame,你需要从DataFrame的第一行开始打印每一条备用行。
输入和预期的输出。
import pandas as pd
df = pd.DataFrame([["A", 1], ["B", 2],
["C", 3], ["D", 4]],
columns=["col_A", "col_B"])
output_df = # YOUR CODE HERE
"""
col_B col_A
0 1 A
2 3 C
"""
解决方案也与上面两个类似。在这里,在定义切片部分时,我们可以指定切片的步骤为2,如下图所示。
import pandas as pd
df.iloc[::2]
"""
col_B col_A
0 1 A
2 3 C
"""
- 在任意位置插入一个行
提示。与前面的任务类似,你将得到相同的DataFrame。你的任务是在DataFrame的特定索引处插入一个给定的列表并重新分配索引。
输入和预期的输出。
import pandas as pd
df = pd.DataFrame([["A", 1], ["B", 2], ["C", 3], ["D", 4]],
columns=["col_A", "col_B"])
insert_pos = 1
insert_row = ["P", 5]
output_df = # YOUR CODE HERE
"""
col_A col_B
0 A 1
1 P 5
2 B 2
3 C 3
4 D 4
"""
解决方案。
给定一个插入位置,首先将新行分配到给定索引和之前的索引之间的一个索引。这就是赋值语句的作用。接下来,我们在索引上对DataFrame进行排序。最后,我们重新分配索引以消除基于浮点的索引值。
import pandas as pd
df.loc[insert_pos - 0.1] = insert_row
df = df.sort_index()
df = df.reset_index(drop = True)
"""
col_A col_B
0 A 1
1 P 5
2 B 2
3 C 3
4 D 4
"""
- 对DataFrame的每个单元格应用函数
提示。最后你需要将一个给定的函数应用于整个DataFrame。给定的DataFrame仅由整数值组成。任务是通过一个函数使每个条目增加1。
输入和预期的输出。
import pandas as pd
df = pd.DataFrame([[1, 5], [2, 6], [3, 7], [4, 8]],
columns=["col_A", "col_B"])
def func(num):
return num + 1
output_df = # YOUR CODE HERE
"""
col_A col_B
0 2 6
1 3 7
2 4 8
3 5 9
"""
解决方案。
这里我们不使用apply()方法,而是使用applymap()方法,如下图所示。
import pandas as pd
df.applymap(func)
"""
col_A col_B
0 2 6
1 3 7
2 4 8
3 5 9
"""
这就是本次测验的结束,我希望你喜欢尝试这个测验。让我知道你做对了多少。另外,如果你没有注意到,整个测验都是在Jupyter笔记本中进行的,你可以从这里下载。
另外,请坚持下去,因为我打算很快发布更多的练习题。谢谢你的阅读。
Python for Data Analysis A Critical Line-by-Line Review.png数据分析的Python--一个关键的逐行审查
在这篇文章中,我将提供我对Wes McKinney的《Python for Data Analysis》(第二版)一书的评论。我的名字是Ted Petrou,我是pandas的专家,也是最近发布的Pandas Cookbook的作者。我彻底读完了PDA,并创建了一个很长的、可在github上获得的评论。这篇文章提供了那篇完整评论中的一些亮点。
什么是关键的逐行审查?
我读这本书,就好像我是唯一的技术审查员,我被指望找到所有可能的错误。每一行代码都被仔细检查和探索,看看是否存在更好的解决方案。在过去的18个月里,我几乎每天都在写和谈论pandas,我已经对如何使用它形成了强烈的意见。这种批判性的检查导致我发现了相当大比例的代码的错误。
审查的重点是Pandas
PDA的主要重点是pandas库,但它也有关于基本Python、IPython和NumPy的材料,在第1-4章和附录中都有涉及。pandas库在第5-14章中涉及,将是本评论的主要焦点。
PDA的总体总结
PDA类似于一本参考手册
PDA的写法很像参考手册,有条不紊地涵盖了一个功能或操作,然后再转到下一个。如果你想以类似的方式学习pandas,当前版本的官方文档是一个更全面的参考指南。
几乎没有数据分析
实际的数据分析非常少,几乎没有教授对数据有意义的常用技术或理论。
使用随机生成的数据
绝大多数的例子都使用随机生成的或设计好的数据,与现实世界中的数据几乎没有任何相似之处。
操作是孤立学习的
在大多数情况下,操作是孤立学习的,与pandas库的其他部分无关。这并不是现实世界中数据分析的方式,在现实世界中,来自库中不同部分的许多命令将被组合在一起,以获得一个理想的结果。
已经过时了
尽管这些命令对当前的pandas 0.21版本有效,但很明显,这本书并没有更新到0.18版本,也就是2016年3月发布的。这一点很明显,因为在0.19版本中,重采样方法获得了on参数,而在PDA中是没有的。强大而流行的函数merge_asof也是在0.19版本中加入的,书中没有提到过一次。
大量的非现代和非直觉的代码
在很多情况下,可以看出这本书没有更新以展示更多的现代代码。例如,take方法几乎不再被使用,而是完全被.iloc索引器所取代。还有很多情况下,代码片段可以通过使用完全不同的语法进行显著的转换,这将导致更好的性能和可读性。
索引的混乱
对于刚接触pandas的人来说,最困惑的事情之一是用索引器[]、.loc和.iloc选择数据的多种方式。这里没有足够详细的解释让读者对每一种方法都有透彻的理解。
逐章回顾
在接下来的章节中,我将对每一章进行简短的总结,然后对具体的代码片断进行更详细的介绍。
第五章 pandas的入门
第5章介绍了主要的pandas数据结构,Series和DataFrame,以及它们中一些比较流行的方法。本章中的命令几乎都可以直接在pandas的官方文档中找到,而且更加深入。
例如,本书一开始就制作了一个几乎是文档介绍部分的复制品。本章中所有的索引选择都在文档中的索引部分有更详细的介绍。像本书的大部分内容一样,这一章使用了随机生成的或臆造的数据,对真实的数据分析没有什么应用。
第五章的细节一开始就错误地指出,Series和DataFrame构造函数经常被使用。其实不然,大多数数据都是通过read_csv函数或其他许多以read_开头的函数直接读入pandas的DataFrame。我也不鼓励人们像这样直接将构造函数导入他们的全局命名空间。
from pandas import Series, DataFrame
相反,更常见的做法是:
import pandas as pd
序列和数据框架的构造函数
第一行真正的代码是用它的构造器创建一个Series。我认为不应该先介绍Series和DataFrame的构造,因为在大多数基本的数据分析中,通常根本不会用到它。
PDA使用变量名obj来指代系列。DataFrames被赋予更好的名字frame,但这并不是整个pandas世界的典型用法。文档中使用s代表系列,df代表DataFrames。有一次,DataFrame被命名为data。无论使用什么名字都应该是一致的。
索引
数据选择的下一步是索引操作,[]在一个系列上。这是个错误。我强烈建议所有的pandas用户不要使用索引操作符本身来选择一个系列中的元素。它是模糊的,而且一点也不明确。相反,我建议总是使用.loc/.iloc来从一个系列中进行选择。唯一推荐使用的索引操作符是在做布尔索引的时候。
例如,PDA是这样做的。
s = pd.Series(data=[9, -5, 13], index=['a', 'b', 'c'] )
s
a 9
b -5
c 13
dtype: int64
索引操作符可以接受整数或标签,因此是模糊的
s[1]
-5
s['b]
-5
当它应该对整数使用.iloc,对标签使用.loc。
代码块
s.iloc[1]
-5
s.loc['b']
5
应用方法
我真的不喜欢这么快就教apply方法,因为初学者几乎总是误用它。几乎总是有更好、更有效的替代方法。在其中一个案例中,apply被用来通过在每一行上调用一个自定义函数来进行聚合。这是完全没有必要的,因为存在一个快速而简单的方法,用内置的pandas方法就可以做到。
数据看起来是这样的。
df = pd.DataFrame(np.random.randn(4, 3), columns=list('abc'),
index=['Quebec', 'Ontario', 'Alberta', 'Nova Scotia'])
df
设置数据
PDA然后像这样使用apply。
df.apply(lambda x: x.max() - x.min())
a 2.600356
b 0.880358
c 2.039398
dtype: float64
一个更习惯的方法是直接使用max和min方法来做这个。
df.max() - df.min()
a 2.600356
b 0.880358
c 2.039398
dtype: float64
我知道这里用了一个编造的例子来理解apply的机制,但这正是新用户会感到困惑的地方。apply是一个迭代的、缓慢的方法,只有在没有其他更有效的、矢量化的解决方案存在时才应该使用。关键是要考虑到矢量的解决方案,而不是迭代的解决方案。我在Stack Overflow上回答问题时经常看到这种错误。
apply的另一种糟糕用法
还有另一种对apply的错误使用,因为agg方法存在一个更简单的选择。让我们先创建数据。
df = pd.DataFrame(np.random.randint(0, 10, (6,3)),
columns=['Table', 'Chair', 'Bed'])
df
PDA像这样找到每一列的最大值和最小值。
def f(x):
return pd.Series([x.min(), x.max()], index=['min', 'max'] )
df.apply(f)
有一个更简单的替代方法,即agg方法,它可以产生相同的输出。
df.agg(['min', 'max'])
第6章. 数据加载、存储和文件格式
第6章涵盖了许多从文件中读取数据到pandas DataFrame的方法。有许多以read_开头的函数可以导入几乎所有类型的数据格式。几乎整个章节在文档的IO工具部分都有更详细的介绍。本章中没有实际的数据分析,只有读写文件的机制。第六章的细节。
PDA没有指出read_csv和read_table是完全相同的函数,只是read_csv默认为逗号分隔的分隔符,而read_table默认为制表符分隔的。完全没有必要同时使用这两个函数,而且在它们之间来回走动会让人困惑。
有一个关于用csv模块迭代文件的章节。PDA用一个实际上不需要进一步处理的文件展示了这个机制。如果有一些实际的处理,它可能会变得更有趣,但没有。
PDA用文档中几乎完全相同的例子来介绍read_html函数。
第七章。数据清理和准备
第7章沿用了同样的模式,使用伪造的和随机产生的数据来展示一些方法的基本机制。它没有实际的数据分析,也没有展示如何将多个pandas操作结合在一起。本章中所有的方法在文档中都有更详细的介绍。请访问Working with missing data部分,了解pandas中缺失值处理的所有详细机制。本章只涉及少量的字符串操作。请访问与文本数据一起工作一节,以获得更多的细节。
这一章包含了大量拙劣和低效的pandas代码。有一个循环填充DataFrame的极端例子,可以用更简洁的代码完成,速度几乎是100倍。
第7章 细节
本章一开始就介绍了isnull方法,这有点令人困惑,因为在第5章中,使用了函数pd.isnull。我认为在这里把方法和函数的区分做得更好就可以了。
PDA不断地使用axis=1来指代方法调用中的列。我强烈建议不要这样做,因为它不太明确,而是建议使用更明确的axis='columns'。
map和apply方法之间应该有一个区别。理想情况下,map应该只在你传递一个字典/系列来把一个值映射到另一个值的时候使用。apply方法只能接受函数,所以当你需要将一个函数应用到你的Series/DataFrame时,应该始终使用。是的,map也有接受函数的能力,其方式与apply相似,但这就是为什么我们需要在这两种方法之间有一个分离的边界。它有助于避免混淆。总而言之。
只有当你想把一个系列中的每个值真正地映射到另一个系列时才使用map。你的映射必须是一个字典或一个系列。
当你有一个函数想作用于系列中的每个成员时,使用apply。
不要把函数传递给映射
例如,有一个系列其数据是这样的
s = pd.Series(['Houston', 'Miami', 'Cleveland'] )
states_map = {'houston':'Texas', 'miami':'Florida',
'Cleveland':'Ohio'}
PDA说要给map方法传递一个函数。
s.map(lambda x: states_map[x.lower()] )
0 Texas #德州
1 Florida #佛罗里达
2 Ohio #俄亥俄州
dtype: Object
但是如果你要使用一个函数,你应该以同样的方式使用apply。它可以得到同样的结果,而且比map有更多的选择。
s.apply(lambda x: states_map[x.lower()] )
实际上,有一个更简单、更习惯的方法来做这个练习。一般来说,你的映射字典会比你的系列的大小小得多。使字典符合你的系列中的值更有意义。在这里,我们可以简单地改变 dictionary 中的键(用 title string 方法)来匹配我们 Series 中的值。
让我们创建一个有100万个元素的Series,并计算其差异。这种较新的方式是6倍的速度。
s1 = s.sample(1000000, replace=True)
%timeit s1.str.lower().map(states_map) # 从PDA上看很慢
427 ms ± 12.1 ms 每个循环(7次运行的平均值±标准差,每次1个循环)
%timeit s1.map({k.title(): v for k, v in states_map.items()})
73.6 ms ± 909 µs per loop (7次运行的平均值±std. dev. ,每次10个循环)
检测和过滤异常值
在本节中,布尔索引被用来查找系列中绝对值大于3的数值。PDA使用NumPy的函数abs。当有相同的pandas方法可用时,不应使用NumPy函数。
s = pd.Series(np.random.randn(1000))
s[np.abs(s) > 3] # 不要使用numpy
s[s.abs() > 3] # 使用pandas方法
在PDA中,有一段特别糟糕的代码,它将一个DataFrame中的值限定在-3和3之间,像这样。
data[np.abs(data) > 3] = np.sign(data) * 3
#糟糕的写法!
Pandas配备了clip方法,将事情简化了不少。
data.clip(-3, 3)
排列和随机抽样
所示的take方法几乎不再被使用。
它的功能被.iloc索引器所取代。
sampler = [5, 2, 8, 10] 。
df.take(sampler) # 过时了 - 不要使用
df.iloc[sampler] # 习以为常
计算指标/虚拟变量
整本书中最没有效率的一段代码出现在这一节。一个DataFrame被构造出来以显示每个电影类型的指标列(0/1)。这段代码创建了一个全零的DataFrame,然后用很少使用的get_indexer索引方法将其填充。
zero_matrix = np.zeros((len(movies), len(genres))
dummies = pd.DataFrame(zero_matrix, columns=genres)
for i, gen in enumerate(movies.genres):
indices = dummies.columns.get_indexer(gen.split('|'))
dummies.iloc[i, indices] = 1
movies_windic = movies.join(dummies.add_prefix('Genre_'))
movies_windic.head()
上面这段代码慢得令人难以置信,而且不符合习性。movies DataFrame只有3,883行,完成它需要惊人的1.2秒。有几种方法会快得多。
一个迭代的方法是简单地创建一个嵌套的字典,并将其传递给DataFrame构造函数。
d = {}
for key, values in movies.genres.items():
for v in values.split('|'):
if v in d:
d[v].update({key:1})
else:
d[v] = {key:1}
df = movies.join(pd.DataFrame(d).fillna(0))
这只需要14毫秒,大约是PDA中使用的方法的85倍。
第8章 数据处理。连接、合并和重塑
第8章是另一个有假数据的章节,涵盖了许多独立操作的非常基本的机制。很少有像现实世界中那样将多个pandas方法结合在一起的代码。分层索引可能会让读者感到困惑,因为它相当复杂,建议阅读分层索引的文档。
关于合并和连接的章节并没有很好地涵盖两者之间的区别,而且例子也很枯燥。请看关于合并的文档以了解更多。
第8章 细节
在一个代码块中,索引操作符[]和.loc都被用来选择系列元素。这很混乱,正如我多次说过的,坚持只使用.loc/.iloc,除非在做布尔索引时。
从 "长 "到 "宽 "格式的转换
最后,我们来到了一个更有趣的情况。这里需要提到的是来自Hadley Wickham的整洁与混乱的数据。第一个使用的数据集实际上是整洁的,根本不需要任何重塑。而当数据被重塑为'长'数据时,它实际上就变得混乱了。
reset_index方法有名称参数,当它成为一个列时,可以重新命名Series的值。没有必要再单独调用。
### 不需要
ldata = data.stack().reset_index().rename(columns={0: 'value'})
### 可以缩短为
ldata = data.stack().reset_index(name='value')
在pandas的0.20版本中,引入了melt方法。PDA使用melt函数,它仍然有效,但是往后,如果方法可用并且做同样的事情,最好还是使用方法。
第9章. 绘图和可视化
Matplotlib的内容非常浅显,没有理解基础知识所需的深度。Matplotlib有两个不同的界面,用户与之交互以产生可视化。这是该库最基本的部分,需要理解。PDA同时使用有状态接口和面向对象的接口,而且有时是在同一个代码块中。matplotlib文档特别警告不要这样做。由于这只是对matplotlib的一个小的介绍,我想如果能涵盖一个单一的接口会更好。
实际的绘图工作主要是用随机数据完成的,而不是在一个真实的环境中进行多个pandas操作。这是一个非常机械的章节,介绍如何使用matplotlib、pandas和seaborn的一些绘图功能。
Matplotlib现在有能力直接接受带有数据参数的pandas DataFrames,这在PDA中是没有的。
本章的另一个主要问题是缺乏对pandas和seaborn绘图理念的比较。Pandas的大部分绘图功能使用宽的或聚合的数据,而seaborn需要整齐的或长的数据。
第九章 细节
如果不澄清用matplotlib做图的两种不同接口,那就是一个大错误。它只是顺便提到甚至有两个接口。你将不得不尝试从代码中推断出哪个是哪个。Matplotlib以其混乱的库而闻名,这一章延续了这种看法。
PDA用figure方法创建了一个图,然后用add_subplot一次次地添加子图。我真的不喜欢这个介绍,因为没有介绍Figure -Axes的层次结构。这个层次结构对于理解matplotlib的一切至关重要,但它甚至没有提到。
当PDA转到用pandas绘图时,只创建了一个简单的线图和几个条形图。还有几个pandas能够创建的图--有些是用一个变量,有些是用两个变量。堆积面积图是我最喜欢的一种,在PDA中没有提到。
所有的pandas绘图都使用df.plot.bar格式,而不是df.plot(kind='bar')。它们产生的结果是一样的,但是df.plot.bar的文档说明是最小的,这使得记住正确的参数名称要困难得多。
用于pandas和seaborn绘图的数据是随seaborn库打包的提示数据集。应该有更多的努力去寻找真正的数据集。
对seaborn库的讨论很少,没有提到返回Axes的seaborn函数与返回seaborn网格的函数之间的区别。这对于理解seaborn是至关重要的。官方的seaborn教程要比PDA中的小总结好得多。
第10章. 数据聚合和分组操作
第10章展示了groupby方法对数据进行分组的各种能力。这可能是本书中比较好的一章,因为它涵盖了相当多的groupby应用的细节,尽管主要是假数据的细节
用下面的语法来介绍groupby机制。
grouped = df['data1'].groupby(df['key1'] )
没有人使用这种语法。我认为应该首先使用更常见的df.groupby('key')['data']。这种更常见的语法在后面的几个代码块中有所涉及,但它应该先被使用。
遍历分组
PDA说,获取组的一个有用的秘诀是如下。
pieces = dict(list(df.groupby('key1') ))
pieces['b']
这样做的工作量太大,已经有get_group方法可以帮你完成这个任务。
g = df.groupby('key')
g.get_group('b')
数据聚合
对于 "聚合 "这个术语,需要有一个更清晰的定义。PDA说一个聚合 "产生标量值"。非常准确地说,它应该说 "产生一个单一的标量值"。
应用:一般分割-应用-合并
本节的第一个例子使用apply groupby方法和一个自定义函数来返回每个吸烟/不吸烟组的最高提示百分比,像这样。
def top(df, n=5, column='tip_pct'):
return df.sort_values(by=column)[-n:]
tips.groupby('smoker').apply(top)
实际上没有必要使用apply,因为它可以先对整个数据框架进行排序,然后分组并取最上面的行,像这样。
tips.sort_values('tip_pct', ascending=False) \.groupby('smoker')。
.groupby('smoker').head()
或者像这样使用第n种方法。
tips.sort_values('tip_pct', ascending=False) \.
.groupby('smoker').nth(list(range(5)))
第11章. 时间序列
关于时间序列的第11章是整本书中最枯燥和机械的一章。几乎所有的命令都是在独立于其他pandas命令的情况下用随机或臆造的数据执行的。
pandas时间序列文档更详细地涵盖了这些材料。
本章的写作时间不晚于 pandas 0.18,因为它没有显示表 11.5 中 resample 方法的 on 参数。这个参数使得我们可以在任何列上使用resample,而不仅仅是索引中的那一列。这个参数是在0.19版本中加入的,到本书发布时,pandas已经是0.20版本了。同样缺失的还有tshift方法,在按时间段移动索引时,它肯定应该被提及,因为它可以直接替代shift方法。
第12章. Pandas高级技巧
这是本书中最短的一章,号称有 "高级 "的Pandas操作。这里涉及的大多数操作都是库的基本操作。关于方法链的部分确实有一些比较复杂的例子。
本章的一个主要缺陷是使用了被废弃的pd.TimeGrouper而不是pd.Grouper。
pd.TimeGrouper的局限性之一是它只能对索引中的时间进行分组。
pd.Grouper可以对索引以外的列进行分组。
此外,还可以在groupby方法之后连锁resample方法,对时间和任何其他列组进行分组。
第12章一些细节值得关注
在本章中使用了.iloc索引器开发之前的take方法。没有人使用这种方法,它肯定应该被.iloc取代。看一下 stackoverflow 的搜索功能,df.take 返回 12 个结果,而 df.iloc 返回 2100 多个结果。.iloc索引器在PDA中应该被更多地使用。
在创建有序分类序列的过程中,应该使用sort_values方法来展示如何根据整数类别代码而不是字符串值本身来进行排序。PDA做了以下工作来排序类别。
categories = ['foo', 'bar', 'baz']
codes = [0, 1, 2, 0, 0, 1].
ordered_cat = pd.Categorical.from_codes(code, categories, ordered=True)
为了说明如何通过代码对类别进行排序,你可以这样做。
pd.Categorical.from_codes(codes, categories).sort_values()
创建用于建模的虚拟变量
本节中使用的例子如果使用整数而不是字符串,可能会更加强大。字符串会自动用pd.get_dummies进行编码。例如,没有必要将字符串这一栏强制为类别。PDA的做法如下。
cat_s = pd.Series(['a', 'b', 'c', 'd'] * 2, dtype='category')
d1 = pd.get_dummies(cat_s)
使用dtype='category'是没有必要的。
s = pd.Series(['a', 'b', 'c', 'd'] * 2)
d2 = pd.get_dummies(s)
d1.equals(d2)
True
一个更好的例子是使用整数。pd.get_dummies忽略了数字列,但确实为每一个分类层次建立了一个新的列,不管其基础数据类型是什么。
s = pd.Series([50, 10, 8, 10, 50] , dtype='category')
pd.get_dummies(s)
分组时间重取样
本节有几个问题。PDA承诺会更新到Pandas 0.20版本,但它没有。让我们看看PDA是如何进行以下操作的。
times = pd.date_range('2017-05-20 00:00', freq='1min',
periods=N)
df = pd.DataFrame({'time': times, 'value': np.arange(N)})
df.set_index('time').resample('5min').count()
使用最新版本的pandas,你可以这样做。
df.resample('5min', on='time).count()
下一个问题出现在同时按时间和另一列分组的时候。PDA使用被废弃的pd.TimeGrouper,它从来没有任何文档。PDA的做法如下。
df2 = pd.DataFrame({‘time’: times.repeat(3),
‘key’: np.tile([‘a’, ‘b’, ‘c’], N),
‘value’: np.arange(N * 3.)})
time_key = pd.TimeGrouper(‘5min’) # 被废弃了,所以永远不要使用
resampled = (df2.set_index(‘time’)
.groupby([‘key’, time_key])
.sum())
pd.TimeGrouper被废弃了,所以永远不要使用
在更现代的pandas中,有两个选项:
df2.groupby(['key', pd.Grouper(key='time', freq='5T')]) .sum()
groupby对象实际上有一个重采样方法:
df2.groupby('key').resample('5T', on='time').sum()
第13章. Python中的建模库介绍
下面这段代码来自PDA,创建了一些假数据和一个分类列。
data = pd.DataFrame({'x0': [1, 2, 3, 4, 5],
'x1': [0.01, -0.01, 0.25, -4.1, 0.],
'y': [-1.5, 0., 3.6, 1.3, -2.]})
data['category'] = pd.Categorical(['a', 'b', 'a', 'a', 'b'],
categories=['a', 'b'])
然后,这个分类列被编码为两个独立的列,每个类别一个。
dummies = pd.get_dummies(data.category, prefix='category')
data_with_dummies = data.drop('category', axis=1).join(dummies)
有一个更简单的方法来做到这一点。你可以直接将整个DataFrame放入pd.get_dummies中,它将忽略数字列,而对所有对象/类别列进行编码,就像这样。
pd.get_dummies(data)
实际上,有一个非常酷的assign、starred args ( **)
和pd.get_dummies的应用,它在一个步骤中完成了从数据创建到最终产品的整个操作。
data = pd.DataFrame({'x0': [1, 2, 3, 4, 5],
'x1': [0.01, -0.01, 0.25, -4.1, 0.],
'y': [-1.5, 0., 3.6, 1.3, -2.]})
data.assign(**pd.get_dummies(['a', 'b', 'a', 'b'])
本章的其余部分简要介绍了patsy、statsmodels和scikit-learn库。
Patsy不是一个常用的库,所以我不知道为什么它甚至出现在书中。
自2017年2月以来,它没有任何提交,在stackoverflow上总共只有46个问题。
与此相比,pandas上的问题有5万多个。
第14章 数据分析实例
第14章终于介绍了真实的数据,并有五个不同的微型探索。
来自Bitly的USA.gov数据
pd.read_json函数可以也应该被用来从bitly读入数据。PDA使数据的读取比它需要的更复杂,因为它没有使用最新版本的pandas。这个函数有一个新的参数,lines,当设置为True时,可以直接读入多行json数据到一个DataFrame中。这是个相当大的遗漏。
frame = pd.read_json('datasets/bitly_usagov/example.txt',
lines=True)
在计算完时区后,PDA使用seaborn来做柱状图,这很好,但在这种情况下,pandas可能是绘图的更好选择。
frame.tz.value_counts().iloc[:10].plot(kind='barh')
有一个seaborn countplot,它确实像这样做计数,而不需要value_counts,但它不排序,也不排序。
sns.countplot('tz', data=frame)
浏览器的展现代码很差,可以大大改进。下面是原版的:
results = pd.Series([x.split()[0] for x in frame.a.dropna()] )
result.value_counts()[:8]
它可以被现代化为下面的方法,它使用str访问器:
frame['a'].str.split().str[0].value_counts().head(8)
MovieLens 1M数据集
读取数据后,至少有250个评分的电影会像这样被找到。
ratings_by_title = data.groupby('title').size()
active_titles = ratings_by_title.index[rating_by_title >= 250]
这本来是一个很好的机会,可以在调用value_counts后使用 "可通过选择调用"。这里也不需要groupby。所以,我们可以把上面的内容修改成这样。
active_titles = data['title'].value_counts() \\
[lambda x: x >= 250].index
这也会是一个使用过滤器groupby方法的机会,在整本书中根本没有使用这个方法。
data.groupby('movie_id')['title'] #数据分组
.filter(lambda x: len(x) >= 250).drop_duplicates().values
美国婴儿名字 1880-2010
如果能看到更新的数据,包括2016年,那就更好。书中甚至有一个数据来源的链接。更新数据是非常容易的,而且应该已经完成了。这表明更新这本书所花的精力很少。
在所有的婴儿名字被串联成一个数据框架后,PDA为每个性别的每个名字增加了一列比例,像这样。
def add_prop(group):
group[‘prop’] = group.births / group.births.sum()
return group
names = names.groupby([‘year’, ‘sex’]).apply(add_prop)
注意,在add_prop函数中,传递的子数据帧被修改并返回。这里不需要返回整个DataFrame。相反,你可以只对出生列使用转换方法。这就更直接了,而且速度也快了一倍。
names['prop'] = names.groupby(['year', 'sex'])['births'] \
.transform(lambda x: x / x.sum())
PDA像这样提取每个婴儿名字的最后一个字母。
get_last_letter = lambda x: x[-1)
last_letters = names.name.map(get_last_letter)
这是令人困惑的,因为我以前提到过,当传递一个函数时,apply和map做同样的事情。我在这里总是使用apply来区分这两者。但是,我们可以做得更好。Pandas有内置的功能,可以像这样从一列字符串中获取任何字符。
names.name.str[-1]
Python for Data Analysis A Critical Line-by-Line Review-2.png
当Leslie的名字从男性变成女性时,整本书中最有趣的分析就出现了。还有很多有趣的事情需要发现。五三八 "几年前有一个关于名字的有趣故事,其中有许多更有趣的图表。请看这个关于 "布列塔尼 "这个名字的图表。
美国农业部食品数据库
书中缺少一整块的代码。它进入了网上的Jupyter笔记本,但我猜它被从实际的书中遗忘了。
书中缺失的代码
# Missing code from book
nutrients = []
for rec in db:
fnuts = pd.DataFrame(rec[‘nutrients’])
fnuts[‘id’] = rec[‘id’]
nutrients.append(fnuts)
nutrients = pd.concat(nutrients, ignore_index=True)
PDA使用一种曲折的方式来寻找具有最多营养素的食物。
by_nutrient = ndata.groupby(['nutgroup', 'nutrition'])
get_maximum = lambda x: x.loc[x.value.idxmax()]
max_foods = by_nutrient.apply(get_maximum)[['value', 'food']]
一个更聪明的方法是对我们想要得到最大值的列进行排序(在本例中为值),然后用drop_duplicates丢弃该组中第一个以外的所有行。你必须在这里使用子集参数。
max_foods = ndata.sort_values('value', ascending=False) \.
.drop_duplicates(subset=['nutritionent'])
2012年联邦选举委员会数据库
这应该是2016年的数据,因为这本书是在2016年选举后一年出版的。
unstack方法应该和fill_value参数一起使用,在数据缺失的地方填上0。它没有这样做,而是返回NaN。
第1-4章和附录
第1章是一个简短的章节,让你掌握了Python和科学库的设置。第2章和第3章迅速涵盖了Python和IPython命令外壳的基础知识。如果你没有Python的经验,这些章节会有一些帮助,但它们本身并不足以让你为本书的材料做好准备。
PDA的范围因涵盖基本的Python而延伸得太远,最好是直接省略它。第3章和关于NumPy的附录更有帮助,因为pandas是直接建立在它之上的,在数据分析过程中,这些库经常交织在一起。
结语。更多的数据分析和更多的真实数据
Python for Data Analysis》只教了如何使用pandas的几个命令的基本机制,很少做实际的数据分析。如果有第三版,我建议把重点放在使用真实的数据集和更高级的分析上。
网友评论