数据科学家必须掌握的5个Python概念

TL;DR · AI 摘要
本文介绍了数据科学家必须掌握的5个Python核心概念,重点强调了NumPy向量化和广播机制在提升数据处理性能中的关键作用,通过对比传统循环与向量化实现,展示了性能提升可达26倍以上。
核心要点
- 使用NumPy向量化可将数组运算速度提升至传统Python循环的26倍以上
- NumPy广播机制允许不同形状数组进行元素级运算而无需复制数据
- 数据科学中应避免使用标准Python循环处理大规模数据集
结构提纲
按章节快速跳转。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- 5 Must-Know Python Concepts for Data Scientists
- NumPy Vectorization
- Avoid Python loops
- Use pre-compiled C extensions
- Achieve 26x speedup
- Broadcasting
- Handle mismatched dimensions
- No data duplication
- Element-wise operations
- Performance Optimization
- Replace procedural code
- Use vectorized approaches
- Memory-aware programming
金句 / Highlights
值得收藏与分享的关键句。
标准Python循环很慢,因为每次迭代都会产生大量开销:类型检查、动态方法查找和引用计数。
通过向量化算术运算,我们可以用更简洁、更清晰的代码实现巨大的性能提升。
广播允许对不同形状的数组进行元素级运算,通过在缺失或单元素维度上虚拟扩展较小的数组,而无需复制任何内存数据。
数据科学家必须掌握的 5 个 Python 概念 - KDnuggets
URL 来源: https://www.kdnuggets.com/5-must-know-python-concepts-for-data-scientists
发布日期: 2026 年 6 月 2 日 星期二 06:08:31 GMT

#引言
你不应该仅仅因为“别人都在用”就使用 Python 进行数据分析!Python 在数据领域的主导地位并非偶然。它是一种基于高度表达性、可读性强的语法构建的语言,抽象掉了底层的内存管理。然而,这种高级抽象也带来了代价:标准 Python 执行是动态类型和解释型的,这可能导致原始迭代过程变得非常缓慢。
为了编写高性能的数据系统,数据科学家必须从标准的过程式编码模式转向专门的、向量化和内存感知的方法。在本文中,我们将深入探讨五个必须了解的 Python 概念,帮助你从编写笨拙、缓慢的意大利面代码,转变为构建闪电般快速、生产级且功能优美的数据管道。
#1. NumPy 向量化
标准 Python 循环速度很慢。由于 Python 是一种解释型语言,每次 for 循环的迭代都会产生显著的开销:类型检查、动态方法查找和引用计数。当你处理数百万个数据点时,这些微小的开销会累积成几秒的瓶颈。
解决方案是 NumPy 的向量化。与在 Python 字节码中顺序处理元素不同,NumPy 将循环卸载到高度优化的预编译 C 扩展中。这些操作一次作用于整个数组,以机器级别执行连续的数组块,通常利用单指令多数据(SIMD)指令。
#### //笨拙的方式
假设我们有一个包含一百万个浮点值的列表,代表原始传感器读数,我们需要将每个读数乘以 1.5,并应用一个校准常数 10.0。使用显式的 Python 循环:
import time
# 一个包含 1000 万个传感器读数的大列表
n_elements = 10_000_000
data_list = [float(x) for x in range(n_elements)]
# 使用显式 Python 循环缩放值
start_time = time.time()
scaled_list = []
for val in data_list:
scaled_list.append(val * 1.5 + 10.0)
loop_duration = time.time() - start_time
print(f"循环实现耗时: {loop_duration:.6f} 秒")输出:
循环实现耗时: 0.378866 秒
#### //向量化的方式
这是优雅的向量化替代方案。我们将数据加载到连续的 NumPy 数组中,并直接对数组对象执行算术运算:
import numpy as np
import time
# 一个包含 1000 万个传感器读数的大列表
n_elements = 10_000_000
# 向量化方式:NumPy 在预编译的 C 循环中执行整个计算
data_array = np.arange(n_elements, dtype=float)
start_time = time.time()
scaled_array = data_array * 1.5 + 10.0
numpy_duration = time.time() - start_time
print(f"NumPy 实现耗时: {numpy_duration:.6f} 秒")
print(f"提速: {loop_duration / numpy_duration:.1f} 倍更快!")输出:
循环实现耗时: 0.348456 秒
NumPy 实现耗时: 0.013395 秒
提速: 26.0 倍更快!通过向量化算术运算,我们可以实现巨大的性能提升,同时代码更清晰、更简洁。循环从 Python 空间中移除,完全在高速 C 空间中执行。
#2. 广播:不匹配维度的数学规则
在线性代数中,矩阵运算通常要求两个操作数具有完全相同的形状。然而,在数据科学中,我们经常需要对不同维度的数组执行操作,例如从数据集中减去特征列的平均值,或归一化行值。
为了避免复制数据以强制匹配形状,NumPy 使用一组称为 广播 的数学规则。广播允许在不同形状的数组上进行逐元素操作,通过虚拟地沿缺失或单元素维度扩展较小的数组,而无需在内存中复制任何数据。
广播规则如下:
- 如果数组的秩(维度数量)不相同,则在较低秩数组的形状前添加 1s,直到两个形状长度相同
- 两个维度兼容,如果它们相等,或者其中一个为 1
- 如果兼容,数组的行为就像沿着大小为 1 的维度拉伸以匹配另一个数组的形状一样
#### //笨拙的方式
假设我们有一个 3x4 特征矩阵(3 个样本,4 个特征),并希望减去列均值以“去均值”特征:
import numpy as np
features = np.array([
[10.0, 20.0, 30.0, 4.0],
[12.0, 24.0, 36.0, 8.0],
[14.0, 28.0, 42.0, 12.0]
])
# 每个特征列的均值(形状:(4,))
col_means = np.mean(features, axis=0)
# 使用嵌套循环手动去均值
demeaned_clunky = np.zeros_like(features)
for idx in range(features.shape[0]):
for col_idx in range(features.shape[1]):
demeaned_clunky[idx, col_idx] = features[idx, col_idx] - col_means[col_idx]
# 替代方案:平铺数组以强制匹配形状
tiled_means = np.tile(col_means, (features.shape[0], 1))
demeaned_tiled = features - tiled_means#### //Python 风格的方式
通过广播,我们可以直接执行减法。NumPy 通过将列均值形状视为 (1, 4),自动将 (3, 4) 特征矩阵与 (4,) 列均值数组对齐:
import numpy as np
features = np.array([
[10.0, 20.0, 30.0, 4.0],
[12.0, 24.0, 36.0, 8.0],
[14.0, 28.0, 42.0, 12.0]
])
col_means = np.mean(features, axis=0)
# 通过自动广播进行 Python 风格的减法
demeaned_broadcasting = features - col_means按行求和进行逐行归一化
row_sums 的形状为 (3,) -> 为了将 (3, 4) 除以 (3,),我们使用 np.newaxis 扩展形状为 (3, 1)
row_sums = np.sum(features, axis=1) normalized_features = features / row_sums[:, np.newaxis]
print("去均值:\n", demeaned_broadcasting) print("\n归一化行:\n", normalized_features)
输出:
去均值: [[-2. -4. -6. -4.] [ 0. 0. 0. 0.] [ 2. 4. 6. 4.]]
归一化行: [[0.15625 0.3125 0.46875 0.0625 ] [0.15 0.3 0.45 0.1 ] [0.14583333 0.29166667 0.4375 0.125 ]]
广播机制消除了重复值和内存复制。在底层,NumPy 以 C 语言的速度运行减法循环,而无需创建临时的 tiled 矩阵,从而保留了内存带宽并加速了操作。
## #3. Pandas 的 .pipe() 和 .assign() 方法:简洁、功能化的数据管道
在 [Pandas](https://pandas.pydata.org/) 中,数据准备通常会退化为顺序的混乱代码。开发者会创建多个中间 DataFrame(如 df1、df2 等),就地修改变量,或链式调用方括号。这会导致代码难以阅读、难以测试,并且极易出现令人头疼的 `SettingWithCopyWarning` 警告。
现代 Pandas 鼓励从过程式修改转向功能化、声明式的数据管道。通过利用 [`.assign()`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.assign.html) 进行特征创建和 [`.pipe()`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.pipe.html) 进行可重用的多列操作,你可以在单个管道中链接步骤。
#### // 传统方式
让我们处理一个原始的客户销售数据集,需要过滤异常值、标准化字符串、填充缺失值并计算销售税。
import pandas as pd import numpy as np
raw_data = { 'Customer_ID': [101, 102, 103, 104, 105], 'Age': [25, -5, 47, 120, 31], 'Country': ['usa', 'CANADA', 'usa', 'Germany', 'canada'], 'Raw_Spend': [120.50, 450.00, 80.00, np.nan, 300.00] } df = pd.DataFrame(raw_data)
顺序的中间变异
df_clean = df.copy()
1. 过滤无效年龄
df_clean = df_clean[(df_clean['Age'] >= 0) & (df_clean['Age'] <= 100)]
2. 标准化国家名称(可能导致复制警告)
df_clean['Country'] = df_clean['Country'].str.upper().str.strip()
3. 填充缺失的 Raw_Spend 值
median_spend = df_clean['Raw_Spend'].median() df_clean['Raw_Spend'] = df_clean['Raw_Spend'].fillna(median_spend)
4. 计算 Taxed_Spend
df_clean['Taxed_Spend'] = df_clean['Raw_Spend'] * 1.15
5. 格式化列名
df_clean = df_clean.rename(columns={'Customer_ID': 'customer_id'})
#### // Pythonic 方式
将此问题视为一个功能方法链问题,我们可以将国家标准化步骤封装成一个可重用的实用函数,并构建一个单一、简洁、自包含的管道。
import pandas as pd import numpy as np
raw_data = { 'Customer_ID': [101, 102, 103, 104, 105], 'Age': [25, -5, 47, 120, 31], 'Country': ['usa', 'CANADA', 'usa', 'Germany', 'canada'], 'Raw_Spend': [120.50, 450.00, 80.00, np.nan, 300.00] } df = pd.DataFrame(raw_data)
用于 .pipe() 的可重用自定义转换函数
def standardize_countries(dataframe: pd.DataFrame) -> pd.DataFrame: df_out = dataframe.copy() df_out['Country'] = df_out['Country'].str.upper().str.strip() return df_out
单一优雅的功能管道
df_clean_pipeline = ( df.query("Age >= 0 and Age <= 100") .assign( Raw_Spend=lambda x: x['Raw_Spend'].fillna(x['Raw_Spend'].median()), Taxed_Spend=lambda x: x['Raw_Spend'] * 1.15 ) .pipe(standardize_countries) .rename(columns={'Customer_ID': 'customer_id'}) )
print(df_clean_pipeline)
输出:
customer_id Age Country Raw_Spend Taxed_Spend 0 101 25 USA 120.5 138.5750 2 103 47 USA 80.0 92.0000 4 105 31 CANADA 300.0 345.0000
方法链确保你的原始 DataFrame 的状态永远不会被意外修改,防止副作用。`.assign()` 通过接收一个 lambda 函数来处理列赋值,其中 `x` 指代链中该点的 DataFrame 当前状态,而 `.pipe()` 允许自定义操作被干净地模块化。
## #4. 使用 Lambda 函数进行数据转换
特征工程经常需要小型的、单一用途的转换,例如格式化字符串、拆分值或应用条件语句。为这些简单计算编写自定义命名函数(使用 `def`)会给脚本增加不必要的样板代码。
更优雅的方法是在 Pandas 的 [`.map()`](https://pandas.pydata.org/docs/reference/api/pandas.Series.map.html) 和 [`.apply()`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.apply.html) 中使用 [lambda 函数](https://docs.python.org/3/tutorial/controlflow.html#lambda-expressions)。Lambda 函数是匿名的、一次性使用的函数,可以即时定义而无需命名,非常适合快速数据映射和简洁的内联转换。
#### // 传统方式
假设我们有一个员工数据集,需要映射他们的远程工作状态并解析他们的姓氏。常见的错误是编写手动循环或使用 `iterrows()`:
import pandas as pd
df = pd.DataFrame({ 'employee_name': ['john doe', 'jane smith', 'bob johnson'], 'department_code': ['IT_01', 'HR_02', 'IT_03'], 'is_remote': [1, 0, 1] })
逐行迭代(缓慢且管理繁琐)
df_clunky = df.copy() df_clunky['remote_status'] = None df_clunky['last_name'] = None
for index, row in df_clunky.iterrows():
解析远程状态
if row['is_remote'] == 1: df_clunky.at[index, 'remote_status'] = "Remote" else: df_clunky.at[index, 'remote_status'] = "Office"
解析并大写姓氏
name_parts = row['employee_name'].split() df_clunky.at[index, 'last_name'] = name_parts[1].capitalize()
#### // Pythonic 方式
以下是使用内联 lambda 转换的简洁、声明式方法。我们通过 .map() 进行简单的转换,通过 .apply() 进行自定义字符串操作,即时转换列:
import pandas as pd
df = pd.DataFrame({
'employee_name': ['john doe', 'jane smith', 'bob johnson'],
'department_code': ['IT_01', 'HR_02', 'IT_03'],
'is_remote': [1, 0, 1]
})
# 嵌套在 map() 和 apply() 中的 lambdas
df_opt = df.assign(
remote_status=lambda d: d['is_remote'].map(lambda val: "Remote" if val == 1 else "Office"),
last_name=lambda d: d['employee_name'].apply(lambda name: name.split()[-1].capitalize()),
dept_level=lambda d: d['department_code'].apply(lambda code: code.split('_')[-1])
)
print(df_opt[['employee_name', 'last_name', 'remote_status', 'dept_level']])输出:
employee_name last_name remote_status dept_level
0 john doe Doe Remote 01
1 jane smith Smith Office 02
2 bob johnson Johnson Remote 03使用 lambda 允许你编写自包含的转换逻辑,使你的逻辑紧密绑定到列创建语句中。通过将 lambda 与 .map() 和 .apply() 结合使用,你可以消除冗长的嵌套循环,并保持代码的可读性。
#5. 数据框的内存管理:优化数据类型
默认情况下,当 Pandas 导入数据集(例如来自 CSV 或数据库文件)时,它会采取保守策略。整数被加载为 64 位(int64),小数为 64 位(float64),文本列则为通用的 object 类型。虽然安全,但这种默认设置会导致最大的内存占用。一个只有几十万行的数据集可以迅速消耗数 GB 的系统 RAM,导致本地运行变慢或在生产服务器上出现“内存不足”错误。
我们可以通过将数值列向下转换为较小的整数/浮点数,并将低基数文本列转换为 category 数据类型,大幅减少 DataFrame 的内存占用。
例如,年龄列的值范围从 0 到 100,可以轻松放入单个 8 位整数(int8,可容纳最多 127)而不是标准的 64 位(int64)数据类型。同样,类别值在底层将文本字符串映射为简单的整数编码,从而实现巨大的空间节省。
#### // 冗长的方式
让我们生成一个 10 万用户的合成订阅者数据集,并查看默认 Pandas 类型所消耗的内存:
import pandas as pd
import numpy as np
n_rows = 100_000
np.random.seed(42)
df_large = pd.DataFrame({
'user_id': np.random.randint(1000000, 1000000 + n_rows, size=n_rows),
'age': np.random.randint(18, 90, size=n_rows),
'device_type': np.random.choice(['iOS', 'Android', 'Web', 'SmartTV'], size=n_rows),
'monthly_revenue': np.random.uniform(5.0, 150.0, size=n_rows),
'active_subscriber': np.random.choice([0, 1], size=n_rows)
})
# 检查内存使用情况
print(df_large.info(memory_usage='deep'))
memory_before = df_large.memory_usage(deep=True).sum() / (1024 ** 2)
print(f"默认内存使用量: {memory_before:.2f} MB")输出:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 100000 entries, 0 to 99999
Data columns (total 5 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 user_id 100000 non-null int64
1 age 100000 non-null int64
2 device_type 100000 non-null object
3 monthly_revenue 100000 non-null float64
4 active_subscriber 100000 non-null int64
dtypes: float64(1), int64(3), object(1)
memory usage: 8.2 MB
None
默认内存使用量: 8.20 MB#### // Pythonic 方式
现在让我们应用我们的优化:将列转换为其所需的最小数值边界,并将文本列转换为 category:
# 向下转换类型
df_optimized = df_large.assign(
user_id=df_large['user_id'].astype('int32'), # 最大 110 万适合 int32
age=df_large['age'].astype('int8'), # 最大年龄 90 适合 int8
device_type=df_large['device_type'].astype('category'), # 低基数(4 个唯一字符串)
monthly_revenue=df_large['monthly_revenue'].astype('float32'), # 单精度浮点数足够
active_subscriber=df_large['active_subscriber'].astype('int8') # 二进制标志适合 int8
)
# 检查优化后的内存使用情况
print(df_optimized.info(memory_usage='deep'))
memory_after = df_optimized.memory_usage(deep=True).sum() / (1024 ** 2)
print(f"优化后内存使用量: {memory_after:.2f} MB")
print(f"内存足迹减少: {((memory_before - memory_after) / memory_before) * 100:.1f}%")输出:
memory usage: 1.0 MB
None
优化后内存使用量: 1.05 MB
内存足迹减少: 87.2%通过简单地调整我们的列 dtypes,我们将 DataFrame 的大小缩小了 近 90%!通过为低基数字符串使用 category,Pandas 避免了在行之间重复字符字符串,而是将每行映射到一个轻量级整数索引。
# 总结
掌握这五个基本的 Python 概念是迈向高级数据科学家的重要一步,能够设计出高效、可读且高度优化的数据管道。
通过利用 NumPy 中的向量化和广播机制,您可以消除原始的 Python 循环,从而实现硬件级别的速度提升。使用 `.pipe()` 和 `.assign()` 将功能型 Pandas 流水线化,可以显著提高特征工程工作流的可读性和安全性。结合内联 `lambda` 函数进行即时转换,并通过 `dtypes` 实现主动内存管理,使您的算法能够无缝地从本地原型扩展到大规模生产负载。
数据科学与软件工程一样,也是一门数学学科。将您的代码视为第一优先级的产品,这样您的数据集处理会更快,您的流水线故障更少,您的系统也会成为一种构建的乐趣。
请务必查看本系列之前的文章:
* [5 个必须知道的 Python 概念](https://www.kdnuggets.com/5-must-know-python-concepts)
* [5 个更多必须知道的 Python 概念](https://www.kdnuggets.com/5-more-must-know-python-concepts)
[](https://www.linkedin.com/in/mattmayo13/)****[Matthew Mayo](https://www.kdnuggets.com/wp-content/uploads/profile-pic.jpg)**** ([**@mattmayo13**](https://twitter.com/mattmayo13)) 拥有计算机科学硕士学位和数据挖掘研究生文凭。作为 [KDnuggets](https://www.kdnuggets.com/) 和 [Statology](https://www.statology.org/) 的主编,以及 [Machine Learning Mastery](https://machinelearningmastery.com/) 的特约编辑,Matthew 致力于让复杂的数据科学概念变得易于理解。他的专业兴趣包括自然语言处理、语言模型、机器学习算法以及探索新兴的人工智能。他致力于推动数据科学社区的知识民主化。Matthew 从6岁起就开始编程。