NumPy: 项目:数据清洗
1. 你将学到
❶ 端到端清洗流程 ❷ 缺失值处理 ❸ 异常值检测(IQR / Z-score) ❹ 去重 ❺ 标准化
2. 故事
Charlie 拿到一份"脏"数据:缺失值、异常值(年龄=999)、重复行、类型混乱。他用 NumPy 一行行清洗——"真实数据从来不是干净的,清洗占 80% 时间。"
(1) 痛点:原始数据充斥缺失、异常、重复和类型混乱,直接分析会得出错误结论。
(2) 解法:用 NumPy 的 isnan/percentile/unique/clip/where 等函数,按流程逐步清洗。
(3) 收益:清洗后的数据可靠、一致,后续建模和分析结果更可信。
3. 数据清洗流程
graph TB
A[Load raw data] --> B[Check: missing/outliers/duplicates/types]
B --> C[Handle missing values]
C --> D[Handle outliers]
D --> E[Deduplicate]
E --> F[Type conversion]
F --> G[Normalize]
G --> H[Validate & save]
style A fill:#f9f,stroke:#333
style H fill:#9f9,stroke:#333
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
真实数据几乎从不"干净"。清洗通常占数据分析 80% 的工作量。NumPy 提供了高效的工具来完成每一步。
4. 缺失值检测与处理
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:NaN 检测(难度⭐)
NumPy 用 np.nan 表示缺失值。检测用 np.isnan:
import numpy as np
data = np.array([1.0, np.nan, 3.5, np.nan, 5.0])
print(np.isnan(data)) # [False True False True False]
print(np.count_nonzero(np.isnan(data))) # 2
print(np.where(np.isnan(data))) # (array([1, 3]),)
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
np.nan == np.nan 返回 False!必须用 np.isnan 检测。
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:缺失值填充(难度⭐⭐)
data = np.array([1.0, np.nan, 3.5, np.nan, 5.0])
# Mean fill
mean_val = np.nanmean(data)
filled = np.where(np.isnan(data), mean_val, data)
print(filled) # [1. 3.167 3.5 3.167 5. ]
# Median fill
median_val = np.nanmedian(data)
filled_med = np.where(np.isnan(data), median_val, data)
print(filled_med) # [1. 3.5 3.5 3.5 5. ]
# Forward fill (use previous valid value)
def forward_fill(arr):
result = arr.copy()
for i in range(1, len(result)):
if np.isnan(result[i]) and not np.isnan(result[i - 1]):
result[i] = result[i - 1]
return result
print(forward_fill(data)) # [1. 1. 3.5 3.5 5. ]
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(2) 缺失值策略对比
| 策略 | 方法 | 适用场景 | 风险 |
|---|---|---|---|
| 删除 | arr[~np.isnan(arr)] |
缺失少(<5%) | 丢失信息 |
| 均值填充 | np.nanmean |
近正态分布 | 降低方差 |
| 中位数填充 | np.nanmedian |
有偏分布/有异常值 | 不改变中位数 |
| 前向填充 | 自定义循环 | 时序数据 | 传播旧值 |
| 常量填充 | np.where(isnan, val, arr) |
已知默认值 | 引入偏差 |
5. 异常值检测与处理
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:IQR 异常值检测(难度⭐⭐)
IQR(四分位距)是最常用的异常值检测方法:
ages = np.array([22, 25, 23, 999, 24, 26, 21, 23, 25, 22, 24, 500])
Q1 = np.percentile(ages, 25)
Q3 = np.percentile(ages, 75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
print(f"Q1={Q1}, Q3={Q3}, IQR={IQR}")
print(f"Normal range: [{lower}, {upper}]")
outliers = ages[(ages < lower) | (ages > upper)]
print(f"Outliers: {outliers}") # [999 500]
# Clip treatment
cleaned = np.clip(ages, lower, upper)
print(cleaned) # [22 25 23 36 24 26 21 23 25 22 24 36]
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:Z-score 异常值检测(难度⭐⭐)
Z-score 衡量值偏离均值多少个标准差:
data = np.array([10, 12, 11, 13, 10, 100, 12, 11, 10, 13])
mean = np.mean(data)
std = np.std(data)
z_scores = (data - mean) / std
print(f"Z-scores: {z_scores.round(2)}")
# [-0.56 -0.34 -0.45 -0.23 -0.56 3.18 -0.34 -0.45 -0.56 -0.23]
threshold = 2.0
outliers = data[np.abs(z_scores) > threshold]
print(f"Outliers: {outliers}") # [100]
# Replace outliers with median
median_val = np.median(data)
cleaned = np.where(np.abs(z_scores) > threshold, median_val, data)
print(cleaned) # [10 12 11 13 10 11 12 11 10 13]
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(2) 异常值检测方法对比
| 方法 | 原理 | 适用分布 | 优点 | 缺点 |
|---|---|---|---|---|
| IQR | 四分位距 | 任意分布 | 鲁棒,不受极端值影响 | 对小样本敏感 |
| Z-score | 标准差 | 近正态 | 直观,标准化尺度 | 受极端值影响均值/标准差 |
| 修改Z-score | MAD | 任意分布 | 比Z-score更鲁棒 | 计算稍复杂 |
6. 去重与类型转换
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:数组去重(难度⭐)
ids = np.array([101, 102, 103, 101, 104, 102, 105])
unique_ids = np.unique(ids)
print(unique_ids) # [101 102 103 104 105]
# Check for duplicates
has_dup = len(ids) != len(unique_ids)
print(f"Has duplicates: {has_dup}") # True
# Find duplicate values
values, counts = np.unique(ids, return_counts=True)
duplicates = values[counts > 1]
print(f"Duplicate values: {duplicates}") # [101 102]
# Keep first occurrence after dedup
_, first_idx = np.unique(ids, return_index=True)
deduped = ids[np.sort(first_idx)]
print(deduped) # [101 102 103 104 105]
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:类型转换(难度⭐⭐)
# Convert string numbers to float
str_arr = np.array(['1.5', '2.3', '3.7', 'missing', '4.1'])
# Direct astype fails, need to handle invalid values first
def safe_to_float(arr, default=np.nan):
result = np.empty(len(arr), dtype=np.float64)
for i, val in enumerate(arr):
try:
result[i] = float(val)
except ValueError:
result[i] = default
return result
float_arr = safe_to_float(str_arr)
print(float_arr) # [1.5 2.3 3.7 nan 4.1]
# Int to float
int_arr = np.array([1, 2, 3])
float_arr2 = int_arr.astype(np.float64)
print(float_arr2.dtype) # float64
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
7. 标准化
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:Min-Max 标准化(难度⭐)
将数据缩放到 [0, 1]:
data = np.array([10, 20, 30, 40, 50])
min_val = np.min(data)
max_val = np.max(data)
normalized = (data - min_val) / (max_val - min_val)
print(normalized) # [0. 0.25 0.5 0.75 1. ]
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:Z-score 标准化(难度⭐)
将数据缩放到均值为 0、标准差为 1:
data = np.array([10, 20, 30, 40, 50])
mean_val = np.mean(data)
std_val = np.std(data)
standardized = (data - mean_val) / std_val
print(standardized.round(2))
# [-1.41 -0.71 0. 0.71 1.41]
print(f"Mean: {np.mean(standardized):.10f}") # ~0
print(f"Std: {np.std(standardized):.2f}") # 1.00
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(2) 标准化方法对比
| 方法 | 公式 | 结果范围 | 适用场景 | 对异常值 |
|---|---|---|---|---|
| Min-Max | (x−min)/(max−min) | [0, 1] | 神经网络、图像 | 敏感 |
| Z-score | (x−μ)/σ | 约[−3, 3] | 统计分析、聚类 | 较敏感 |
| Robust | (x−median)/IQR | 不固定 | 有异常值 | 鲁棒 |
8. 综合示例
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:完整清洗项目(难度⭐⭐⭐)
从加载数据到保存清洗后的结果,端到端完成:
import numpy as np
# -- 1. Load data --
# Simulate dirty data read from CSV
raw = np.array([
[25, 5000, 1],
[30, 6000, 2],
[999, 7000, 1], # Outlier age
[22, np.nan, 2], # Missing salary
[25, 5000, 1], # Duplicate row
[28, 5500, 3],
[35, -500, 2], # Outlier salary (negative)
[np.nan, 4500, 1], # Missing age
[40, 8000, 2],
[22, np.nan, 2], # Missing salary
])
print("=== Raw Data ===")
print(raw)
print(f"Shape: {raw.shape}")
# -- 2. Diagnose issues --
print("\n=== Diagnosis ===")
for col in range(raw.shape[1]):
nan_count = np.count_nonzero(np.isnan(raw[:, col]))
print(f"Col{col}: NaN={nan_count}, "
f"min={np.nanmin(raw[:, col]):.1f}, "
f"max={np.nanmax(raw[:, col]):.1f}")
# -- 3. Handle missing values (column mean fill) --
print("\n=== Missing Values ===")
for col in range(raw.shape[1]):
col_data = raw[:, col]
mask = np.isnan(col_data)
if np.any(mask):
fill_val = np.nanmean(col_data)
raw[mask, col] = fill_val
print(f"Col{col}: Filled {np.count_nonzero(mask)} NaN with mean {fill_val:.1f}")
print(raw)
# -- 4. Handle outliers (IQR clip) --
print("\n=== Outliers ===")
for col in range(raw.shape[1]):
col_data = raw[:, col]
Q1 = np.percentile(col_data, 25)
Q3 = np.percentile(col_data, 75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
outlier_mask = (col_data < lower) | (col_data > upper)
if np.any(outlier_mask):
print(f"Col{col}: IQR=[{lower:.1f}, {upper:.1f}], "
f"Outliers={col_data[outlier_mask]}")
raw[:, col] = np.clip(col_data, lower, upper)
print(raw)
# -- 5. Deduplicate --
print("\n=== Dedup ===")
_, unique_idx = np.unique(raw, axis=0, return_index=True)
raw = raw[np.sort(unique_idx)]
print(f"Before: 10 rows -> After: {raw.shape[0]} rows")
print(raw)
# -- 6. Normalize (Z-score) --
print("\n=== Normalize ===")
for col in range(raw.shape[1]):
col_data = raw[:, col]
mean = np.mean(col_data)
std = np.std(col_data)
if std > 0:
raw[:, col] = (col_data - mean) / std
print(f"Col{col}: mean={mean:.1f}, std={std:.1f}")
print(raw.round(2))
# -- 7. Validate & save --
print("\n=== Final Validation ===")
print(f"NaN count: {np.count_nonzero(np.isnan(raw))}")
print(f"Rows: {raw.shape[0]}, Cols: {raw.shape[1]}")
np.savetxt('cleaned_data.csv', raw, delimiter=',', fmt='%.4f')
print("Saved to cleaned_data.csv")
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
输出:
=== Raw Data ===
[[ 25. 5000. 1.]
[ 30. 6000. 2.]
[999. 7000. 1.]
[ 22. nan 2.]
[ 25. 5000. 1.]
[ 28. 5500. 3.]
[ 35. -500. 2.]
[ nan 4500. 1.]
[ 40. 8000. 2.]
[ 22. nan 2.]]
Shape: (10, 3)
=== Diagnosis ===
Col0: NaN=1, min=22.0, max=999.0
Col1: NaN=2, min=-500.0, max=8000.0
Col2: NaN=0, min=1.0, max=3.0
=== Missing Values ===
Col0: Filled 1 NaN with mean 153.9
Col1: Filled 2 NaN with mean 5062.5
=== Outliers ===
Col0: IQR=[6.0, 44.0], Outliers=[999. 153.9]
Col1: IQR=[1837.5, 8712.5], Outliers=[-500.]
=== Dedup ===
Before: 10 rows -> After: 8 rows
=== Normalize ===
Col0: mean=29.8, std=5.9
Col1: mean=5462.5, std=1206.3
Col2: mean=1.8, std=0.7
=== Final Validation ===
NaN count: 0
Rows: 8, Cols: 3
Saved to cleaned_data.csv
(1) 清洗检查清单
| 步骤 | 检查项 | 方法 |
|---|---|---|
| 1 | 缺失值 | np.isnan + np.count_nonzero |
| 2 | 异常值 | IQR / Z-score + np.percentile |
| 3 | 重复行 | np.unique(axis=0) |
| 4 | 类型 | arr.dtype + astype |
| 5 | 范围 | np.min / np.max |
| 6 | 一致性 | 逻辑验证(如年龄 > 0) |
| 7 | 标准化 | Min-Max / Z-score |
| 8 | 保存 | np.savetxt / np.save |
❓ 常见问题
dropna、fillna、duplicated 等高层 API,用起来更方便,适合表格型数据。NumPy 更底层,操作更灵活,适合数值型数据和大规模计算。实际项目中,Pandas 处理表格清洗更高效,NumPy 适合在 Pandas 内部做数值运算。np.nan == np.nan 返回 False?x != x 能用来检测 NaN,但在 NumPy 中应使用 np.isnan() 来检测,因为某些情况下 x != x 可能不可靠。np.clip 将超出范围的值截断到边界,保留该行数据但压缩了极端值。替换(如用中位数)则完全移除异常信息。clip 适合不想丢数据但需限制范围的场景;替换适合异常值明显错误(如年龄=999)的场景。📖 小节
- 缺失检测:
np.isnan,注意nan != nan,必须用 isnan - 缺失填充:
np.nanmean+np.where,根据分布选均值/中位数 - IQR 异常值:
np.percentile+np.clip,鲁棒,不受极端值影响 - Z-score 异常值:
(x−μ)/σ,适合正态分布,阈值通常取 2 或 3 - 去重:
np.unique(axis=0),return_index保留首次出现 - 类型转换:
astype,先处理无效值再转换 - Min-Max 标准化:
(x−min)/(max−min),结果 [0,1],对异常值敏感 - Z-score 标准化:
(x−μ)/σ,结果均值0标准差1 - 保存:
np.savetxt/np.save,CSV 文本 / 二进制
📝 作业
- 基础题(难度⭐):给定以下脏数据,完成完整清洗流程:
dirty = np.array([
[1.2, 3.4, np.nan],
[1.2, 3.4, 5.6], # Duplicate
[999, 2.1, 4.5], # Outlier
[1.5, np.nan, 6.7],
[0.8, 3.0, 4.2],
[1.2, 3.4, 5.6], # Duplicate
])
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
要求:处理缺失值 → 处理异常值 → 去重 → 输出清洗后数据。
-
进阶题(难度⭐⭐):创建一个含 20% 缺失值的数组(长度 100),分别用均值、中位数、0 填充,对比填充后数组的均值、标准差、中位数变化。
-
挑战题(难度⭐⭐⭐):编写一个函数
clean_data(arr, missing_strategy='mean', outlier_method='iqr', normalize='zscore'),支持:- 缺失值策略:
'mean'、'median'、'drop' - 异常值方法:
'iqr'、'zscore' - 标准化方法:
'minmax'、'zscore'、'none'
函数返回清洗后的数组和清洗报告(处理了多少缺失值、异常值等)。
- 缺失值策略: