《Effective Python》是Brett Slatkin撰写的一本涵盖59种写更好Python代码的具体方法的书籍。该书以随机访问的方式编写,每个主题都有独立的源代码。对于中级Python程序员来说,无论是工程师还是数据科学家,这是一个很好的资源,因为它涵盖了可以以任何顺序学习的广泛主题范围。
许多涵盖的主题非常适用于数据科学工作流。例如,它涵盖了遵循PEP8的Pythonic思维方式,PEP8是一个确保Python代码可读性的风格指南。它涵盖了函数、类和元类的最佳实践,这些在数据科学工作流中有重要用途。它还介绍了编写可读的列表、元组和字典推导式的最佳方法。这可以应用于诸如特征工程、数据预处理和数据后处理等任务。
推导式是一种以可读的方式从另一个列表中派生出一个列表的有用方法。《Effective Python》介绍了推导式的最佳实践(也适用于元组和字典)。它不建议使用map和filter,虽然它们可以实现与推导式相同的任务,但代码更繁琐,更难阅读。它还建议不要在推导式中使用多个表达式。最后,它建议对需要处理大量数据的推导式使用生成器。
在数据科学工作流中编写高效的Python可以确保用于诸如特征工程、数据预处理和数据后处理等任务的代码是高效且易于阅读的。效率和可读性使得数据科学和机器学习代码库更容易维护。代码越容易阅读,对其进行更改而不会引入错误就越容易。此外,了解执行任务的更有效方法(如列表推导式)最终有助于开发人员编写更有效的代码。
接下来我们将看到如何将三个有效的Python实践融入简单的数据科学工作流中。我们将看到使用列表推导式而不是map和filter可以提高可读性。我们还将看到将推导式限制为最多两个表达式可以确保代码清晰度。最后,我们将比较推导式和生成器在处理大量数据时的情况。
在这项工作中,我将在Deepnote中编写代码,这是一个协作的数据科学笔记本,可以轻松运行可重现的实验。我们将使用医疗保健费用数据集。该数据是可以自由使用、修改和共享的,遵循数据库内容许可证(DbCL:公共领域)。
使用列表推导式而不是map和filter Map和filter是Python中的内置函数,提供了可以通过列表推导式实现的任务的便捷方式。为了演示这些技术之间的差异,我们将考虑两种常见的数据任务。具体来说,我们将展示如何使用map生成列的对数变换,然后展示如何使用列表推导式完成相同的任务。
首先,让我们转到Deepnote并创建一个新项目(如果您还没有帐户,可以免费注册)。
让我们创建一个名为“effective_python”的项目,并在此项目中创建一个名为“list_comp_generators”的笔记本。还可以将insurance.csv文件拖放到页面左侧标有“FILES”的面板上:
接下来,让我们导入pandas库并将数据读取到pandas数据帧中:
import pandas as pd df = pd.read_csv("insurance.csv" )
然后让我们显示前五行数据:
df.head()
使用Map进行对数变换
我们首先考虑的列转换是对数变换。这是一种常用的技术,用于将偏斜的数据转换为近似正态分布的数据。我们可以使用map()函数来转换数据中的一列数字值。让我们使用map来对BMI值进行对数变换。我们将导入Numpy库并定义一个接受列表作为输入并返回对数变换元素列表的函数:
import numpy as np def log_transform (input_list) : return np.log(input_list)
接下来,我们可以使用map将我们的函数应用于BMI列表。map函数接受我们将要应用的函数名称和一个可迭代对象(在我们的情况下是一个列表):
output_list = map(function, list)
我们继续定义一个名为bmi_list的变量,并将BMI值列表存储在该变量中。然后,我们可以将我们的函数和列表传递给内置的map函数,并将结果存储在一个新列表中,我们将其称为bmi_lt_map。然后,我们需要定义一个新列来存储我们的转换列:
bmi_list = list(df['bmi' ]) bmi_lt_map = list(map(log_transform, bmi_list)) df['bmi_lt_map' ] = bmi_lt_map
请注意,这需要5行代码来完成:
import numpy as np def log_transform (input_list) : return np.log(input_list) bmi_list = list(df['bmi' ]) bmi_lt_map = map(log_transform, bmi_list) df['bmi_lt_map' ] = bmi_lt_map
使用列表推导式进行对数变换
让我们看看如何使用列表推导式可以帮助我们以更易理解的代码实现相同的功能:
df['bmi_lt_listcomp' ] = [np.log(bmi) for bmi in list(df['bmi' ])]
我们看到我们可以只用两行代码完成完全相同的事情。而且更容易阅读。值得注意的是,直接将对数变换应用于数据帧列也很紧凑且易于阅读,尽管对于更复杂的转换可能不可行。
df['bmi_lt_direct' ] = np.log(df['bmi' ])
避免在列表推导式中使用两个以上的表达式 假设我们有一个由分类机器学习模型生成的预测概率列表的列表:
my_predictions = [[0.5
, 0.2 , 0.8 ], [0.3 , 0.1 , 0.9 ], [1.0 , 0.2 , 0.7 ]]
我们可以使用列表推导式将这个列表的列表转换为一个单一的列表(我们可以“扁平化”列表):
flattened_predictions = [prob for row in my_predictions for prob in row] print(flattened_predictions)
[0.5 , 0.2 , 0.8 , 0.3 , 0.1 , 0.9 , 1.0 , 0.2 , 0.7 ]
这是一个包含两个for循环的列表推导式。这是使用两个传统for循环的易读替代方法:
flat_fl = []for row in my_predictions: for prob in row: flat_fl.append(prob) print(flat_fl)
[0.5
, 0.2 , 0.8 , 0.3 , 0.1 , 0.9 , 1.0 , 0.2 , 0.7 ]
列表推导式方法占用两行代码,而传统for循环占用5行代码!尽管列表推导式非常方便,易于阅读,但随着表达式超过两个,可读性和紧凑性迅速减弱。假设我们想生成一个包含标签“Yes”的列表,概率大于0.8,“Maybe”的概率在0.5到0.8之间,“No”的概率小于0.5的概率列表。我们可以使用列表推导式来实现:
ml_labels = [['Yes' if prob >= 0.8 else 'Maybe' if (prob > 0.5 and prob 0.8 ) else 'No' for prob in row] for row in my_predictions] print(my_predictions) print(ml_labels)
[[0.5 , 0.2 , 0.8 ], [0.3 , 0.1 , 0.9 ], [
1.0 , 0.2 , 0.7 ]] [['No' , 'No' , 'Yes' ], ['No' , 'No' , 'Yes' ], ['Yes' , 'No' , 'Maybe' ]]
尽管这很紧凑,但很难阅读和理解。如果我们使用传统的for循环,尽管需要更多的代码行,但更容易理解:
fl_labels = []for row in my_predictions: hold_list = [] for prob in row: if prob >= 0.8 : hold_list.append('Yes' ) elif (prob > 0.5 and prob 0.8 ): hold_list.append('Maybe' ) else : hold_list.append('No' ) fl_labels.append(hold_list) print(my_predictions) print(fl_labels)
[[0.5 , 0.2 , 0.8 ], [
0.3 , 0.1 , 0.9 ], [1.0 , 0.2 , 0.7 ]] [['No' , 'No' , 'Yes' ], ['No' , 'No' , 'Yes' ], ['Yes' , 'No' , 'Maybe' ]]
一个好的经验法则是避免在列表推导式中使用两个或更多表达式,包括for循环和条件。
用于大型输入的生成器表达式 虽然列表推导式非常有用,紧凑且易于理解,但对于大型输入,它们可能需要大量内存。用于大型输入的列表推导式的替代方法是生成器表达式。
生成器表达式将推导式和生成器结合在一起。它们非常适合大型输入,因为它们从表达式中逐个生成项目。要编写生成器表达式,我们只需使用括号()。假设我们有一个大型的分类预测概率列表。我们将使用Numpy的random.normal方法生成一个具有均值0.5和标准差0.1的合成列表,其中包含2亿个预测概率:
import numpy as np mu, sigma = 0.5 , 0.1 np.random.seed(42 ) probs = np.random.normal(mu, sigma, 200000000 ) print(probs[:10 ])
[0.54967142 0.48617357 0.56476885 0.65230299 0.47658466
0.4765863 0.65792128 0.57674347 0.45305256 0.554256 ]
现在假设我们想使用列表推导式从中生成一个标签列表。我们将为大于0.5的概率分配一个标签“是”,并为小于0.5的概率分配一个标签“否”。
如果我们尝试在与列表推导式相同的单元格中打印前十个元素,会耗尽内存。
因此,我们需要在单独的单元格中执行:
print(prob_labels[:10 ])
['Yes' , 'No' , 'Yes' , 'Yes' , 'No' , 'No' , 'Yes' , 'Yes' , 'No' , 'Yes' ]
生成器表达式可以用于评估一个迭代器,该迭代器逐个生成项目,而不是整个表达式。这可以帮助避免内存分配问题:
prob_generator = ('Yes' if prob > 0.5 else 'No' for prob in probs) probs_sublist = []for i in range(0 , 11 ): probs_sublist.append(next(prob_generator)) print(probs_sublist)
['Yes' , 'No' , 'Yes' , 'Yes' , 'No' , 'No' , 'Yes' , 'Yes' , 'No' , 'Yes' , 'No' ]
我们看到我们能够创建生成器对象并打印前十个元素,而不会遇到内存问题。
这篇文章中的代码可以在GitHub上找到。
https://github.com/spierre91/deepnote/blob/main/list_comp_generators.ipynb
结论 在本文中,我们讨论了通过Pythonic思维改进列表创建的一些有用方法。
首先,我们讨论了在与Python初学者协作时,应该使用列表推导式而不是map和filter,因为它更容易理解。
然后,我们介绍了在列表推导式中使用两个以上的表达式应该避免以最大化清晰度和可读性。
最后,我们展示了如何使用生成器表达式作为大型输入的列表推导式的替代方法。鼓励您将这些技巧应用到您自己的软件工程和机器学习项目中。
✄----------------------------------------------- 看到这里,说明你喜欢这篇文章,请点击「 在看 」或顺手「 转发 」「 点赞 」。
欢迎微信搜索「 panchuangxx 」,添加小编 磐小小仙 微信,每日朋友圈更新一篇高质量推文(无广告),为您提供更多精彩内容。