NumPy: 项目:数据清洗

1. 你将学到

❶ 端到端清洗流程 ❷ 缺失值处理 ❸ 异常值检测(IQR / Z-score) ❹ 去重 ❺ 标准化


2. 故事

Charlie 拿到一份"脏"数据:缺失值、异常值(年龄=999)、重复行、类型混乱。他用 NumPy 一行行清洗——"真实数据从来不是干净的,清洗占 80% 时间。"

(1) 痛点:原始数据充斥缺失、异常、重复和类型混乱,直接分析会得出错误结论。

(2) 解法:用 NumPy 的 isnan/percentile/unique/clip/where 等函数,按流程逐步清洗。

(3) 收益:清洗后的数据可靠、一致,后续建模和分析结果更可信。


3. 数据清洗流程

100%
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
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

真实数据几乎从不"干净"。清洗通常占数据分析 80% 的工作量。NumPy 提供了高效的工具来完成每一步。


4. 缺失值检测与处理

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:NaN 检测(难度⭐)

NumPy 用 np.nan 表示缺失值。检测用 np.isnan

PYTHON
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]),)
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
⚠️ np.nan == np.nan 返回 False!必须用 np.isnan 检测。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:缺失值填充(难度⭐⭐)

PYTHON
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. ]
TEXT 📖 仅展示
> **输出:** 在本地 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. 异常值检测与处理

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:IQR 异常值检测(难度⭐⭐)

IQR(四分位距)是最常用的异常值检测方法:

PYTHON
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]
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:Z-score 异常值检测(难度⭐⭐)

Z-score 衡量值偏离均值多少个标准差:

PYTHON
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]
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

(2) 异常值检测方法对比

方法 原理 适用分布 优点 缺点
IQR 四分位距 任意分布 鲁棒,不受极端值影响 对小样本敏感
Z-score 标准差 近正态 直观,标准化尺度 受极端值影响均值/标准差
修改Z-score MAD 任意分布 比Z-score更鲁棒 计算稍复杂

6. 去重与类型转换

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:数组去重(难度⭐)

PYTHON
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]
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:类型转换(难度⭐⭐)

PYTHON
# 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
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

7. 标准化

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:Min-Max 标准化(难度⭐)

将数据缩放到 [0, 1]:

PYTHON
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.  ]
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:Z-score 标准化(难度⭐)

将数据缩放到均值为 0、标准差为 1:

PYTHON
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
TEXT 📖 仅展示
> **输出:** 在本地 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. 综合示例

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

:完整清洗项目(难度⭐⭐⭐)

从加载数据到保存清洗后的结果,端到端完成:

PYTHON
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")
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

输出:

TEXT 📖 仅展示
=== 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

❓ 常见问题

Q 缺失值应该删除还是填充?
A 取决于缺失比例和数据重要性。缺失 <5% 可考虑删除;缺失较多建议填充。均值填充适合近正态分布,中位数填充适合有偏分布,前向填充适合时序数据。删除的风险是丢失信息,填充的风险是引入偏差。
Q IQR 和 Z-score 哪个更好?
A IQR 更鲁棒,不受极端值影响,适合非正态数据。Z-score 更直观,适合近似正态分布。如果数据有明显异常值,IQR 更安全。实际中可先用 IQR 粗筛,再用 Z-score 精筛。
Q 标准化总是必要的吗?
A 不是。树模型(如随机森林)不受特征尺度影响,无需标准化。但 KNN、SVM、神经网络、梯度下降类算法对尺度敏感,必须标准化。如果特征量纲差异大(如年龄 0-100 vs 薪资 0-100000),即使不严格必要,标准化也能改善收敛。
Q 如何验证清洗结果?
A 逐项检查:NaN 数量为 0、无超出合理范围的值、去重后行数合理、dtype 正确、标准化后均值≈0 标准差≈1。还可以画直方图看分布是否合理,或对比清洗前后的基本统计量。
Q NumPy 清洗和 Pandas 有什么区别?
A Pandas 提供了 dropnafillnaduplicated 等高层 API,用起来更方便,适合表格型数据。NumPy 更底层,操作更灵活,适合数值型数据和大规模计算。实际项目中,Pandas 处理表格清洗更高效,NumPy 适合在 Pandas 内部做数值运算。
Q 为什么 np.nan == np.nan 返回 False?
A IEEE 754 浮点标准规定 NaN 不等于任何值,包括自身。这是为了让 x != x 能用来检测 NaN,但在 NumPy 中应使用 np.isnan() 来检测,因为某些情况下 x != x 可能不可靠。
Q clip 和替换异常值有什么区别?
A np.clip 将超出范围的值截断到边界,保留该行数据但压缩了极端值。替换(如用中位数)则完全移除异常信息。clip 适合不想丢数据但需限制范围的场景;替换适合异常值明显错误(如年龄=999)的场景。

📖 小节


📝 作业

  1. 基础题(难度⭐):给定以下脏数据,完成完整清洗流程:
PYTHON
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
])
TEXT 📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。

要求:处理缺失值 → 处理异常值 → 去重 → 输出清洗后数据。

  1. 进阶题(难度⭐⭐):创建一个含 20% 缺失值的数组(长度 100),分别用均值、中位数、0 填充,对比填充后数组的均值、标准差、中位数变化。

  2. 挑战题(难度⭐⭐⭐):编写一个函数 clean_data(arr, missing_strategy='mean', outlier_method='iqr', normalize='zscore'),支持:

    • 缺失值策略:'mean''median''drop'
    • 异常值方法:'iqr''zscore'
    • 标准化方法:'minmax''zscore''none'

    函数返回清洗后的数组和清洗报告(处理了多少缺失值、异常值等)。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏