Python爬虫实战:数据质量检测与治理 - 构建健壮的爬虫数据管道(附CSV导出 + SQLite持久化存储)!

㊗️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~

㊙️本期爬虫难度指数:⭐⭐⭐

🉐福利: 一次订阅后,专栏内的所有文章可永久免费看,持续更新中,保底1000+(篇)硬核实战内容。

全文目录:

    • [🌟 开篇语](#🌟 开篇语)
    • [📌 章节摘要](#📌 章节摘要)
    • [🧠 数据质量的七个维度](#🧠 数据质量的七个维度)
      • [1️⃣ 完整性(Completeness)](#1️⃣ 完整性(Completeness))
      • [2️⃣ 准确性(Accuracy)](#2️⃣ 准确性(Accuracy))
      • [3️⃣ 一致性(Consistency)](#3️⃣ 一致性(Consistency))
      • [4️⃣ 时效性(Timeliness)](#4️⃣ 时效性(Timeliness))
      • [5️⃣ 唯一性(Uniqueness)](#5️⃣ 唯一性(Uniqueness))
      • [6️⃣ 有效性(Validity)](#6️⃣ 有效性(Validity))
      • [7️⃣ 可信性(Credibility)](#7️⃣ 可信性(Credibility))
    • [🔧 完整性检测:缺失值处理](#🔧 完整性检测:缺失值处理)
    • [🚨 异常值检测:统计与机器学习方法](#🚨 异常值检测:统计与机器学习方法)
      • 异常值的定义与类型
      • [方法 1:统计学方法(3σ原则)](#方法 1:统计学方法(3σ原则))
      • [方法 2:箱线图法(可视化)](#方法 2:箱线图法(可视化))
      • [方法 3:孤立森林(Isolation Forest)](#方法 3:孤立森林(Isolation Forest))
      • [方法 4:DBSCAN 聚类](#方法 4:DBSCAN 聚类)
    • [🎯 业务规则检测:Boss直聘特定场景](#🎯 业务规则检测:Boss直聘特定场景)
    • [📊 数据质量监控看板](#📊 数据质量监控看板)
      • [DQI (Data Quality Index) 指标体系](#DQI (Data Quality Index) 指标体系)
    • [💡 最佳实践总结](#💡 最佳实践总结)
      • [1. 数据质量检测的实施流程](#1. 数据质量检测的实施流程)
      • [2. 异常值处理策略选择](#2. 异常值处理策略选择)
      • [3. 性能优化建议](#3. 性能优化建议)
    • [📚 本章总结](#📚 本章总结)
    • [🌟 文末](#🌟 文末)
      • [✅ 专栏持续更新中|建议收藏 + 订阅](#✅ 专栏持续更新中|建议收藏 + 订阅)
      • [✅ 互动征集](#✅ 互动征集)
      • [✅ 免责声明](#✅ 免责声明)

🌟 开篇语

哈喽,各位小伙伴们你们好呀~我是【喵手】。

运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO

欢迎大家常来逛逛,一起学习,一起进步~🌟

我长期专注 Python 爬虫工程化实战 ,主理专栏 《Python爬虫实战》:从采集策略反爬对抗 ,从数据清洗分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上

📌 专栏食用指南(建议收藏)

  • ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
  • ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
  • ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
  • ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用

📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅专栏👉《Python爬虫实战》👈,一次订阅后,专栏内的所有文章可永久免费阅读,持续更新中。

💕订阅后更新会优先推送,按目录学习更高效💯~

📌 章节摘要

在爬虫项目中,数据质量问题往往比技术实现更棘手。一个看似完美的爬虫可能采集到大量"脏数据":缺失的薪资字段、异常的负数价格、越界的评分、格式错乱的日期......这些问题如果不在采集阶段处理,会在后续的数据分析、可视化、机器学习环节造成连锁灾难。

本章将系统讲解如何构建生产级的数据质量检测体系,让你的爬虫不仅能"抓"数据,更能"抓好"数据。

读完你将掌握:

  • 🔍 7 种数据质量维度的检测方法(完整性、准确性、一致性、时效性等)
  • 🛠️ 缺失值处理的 5 种策略(删除、填充、推断、标记、上报)
  • 🚨 异常值检测的 6 种算法(统计法、箱线图、孤立森林、DBSCAN等)
  • 📊 实时监控数据质量指标(DQI 指数、异常率、覆盖率)
  • 🏗️ 数据治理流程设计(采集→验证→清洗→标记→入库→审计)

🧠 数据质量的七个维度

在开始编码前,我们需要建立一个数据质量评估框架。参考 ISO 8000 数据质量标准,我们将数据质量分解为 7 个维度:

1️⃣ 完整性(Completeness)

定义:字段是否缺失,记录是否完整。

检测指标

python 复制代码
# 字段级别完整性
completeness_rate = (non_null_count / total_count) * 100

# 记录级别完整性(所有必填字段都不为空)
record_completeness = (complete_records / total_records) * 100

实战案例

python 复制代码
# Boss 直聘职位数据
required_fields = ['job_title', 'salary_range', 'company_name']

# ❌ 不完整的记录
{
    'job_title': 'Python工程师',
    'salary_range': None,  # ← 缺失!
    'company_name': '字节跳动'
}

# ✅ 完整的记录
{
    'job_title': 'Python工程师',
    'salary_range': '20-35K',
    'company_name': '字节跳动'
}

2️⃣ 准确性(Accuracy)

定义:数据是否符合真实情况,是否存在错误。

检测方法

  • 格式验证(正则表达式)
  • 范围验证(薪资不能为负)
  • 业务规则验证(最低薪资 < 最高薪资)

实战案例

python 复制代码
# ❌ 不准确的数据
{
    'salary_range': '35-20K',  # ← 最小值 > 最大值,逻辑错误
    'experience': '10-3年',    # ← 同上
    'company_scale': '-500人'  # ← 负数,明显错误
}

# ✅ 准确的数据
{
    'salary_range': '20-35K',
    'experience': '3-5年',
    'company_scale': '500-1000人'
}

3️⃣ 一致性(Consistency)

定义:同一实体在不同记录中的数据是否一致。

检测方法

  • 同一公司的名称格式是否统一
  • 同一职位的标签是否重复

实战案例

python 复制代码
# ❌ 不一致的数据
record_1 = {'company_name': '北京字节跳动科技有限公司'}
record_2 = {'company_name': '字节跳动'}
record_3 = {'company_name': 'ByteDance'}  # 同一公司,三种写法

# ✅ 一致的数据(标准化后)
all_records = {'company_name': '字节跳动'}

4️⃣ 时效性(Timeliness)

定义:数据是否过时。

检测方法

  • 采集时间戳距今时长
  • 发布时间是否在合理范围内

实战案例

python 复制代码
from datetime import datetime, timedelta

# ❌ 过时的数据
{
    'job_title': 'Python工程师',
    'crawl_time': '2023-01-01',  # ← 3年前的数据
    'publish_time': '2022-12-25'
}

# ✅ 时效性良好
{
    'job_title': 'Python工程师',
    'crawl_time': '2026-01-31',
    'publish_time': '2026-01-30'
}

5️⃣ 唯一性(Uniqueness)

定义:记录是否重复。

检测方法

  • 基于 URL 去重
  • 基于内容哈希去重

实战案例

python 复制代码
# ❌ 重复数据
df = pd.DataFrame([
    {'job_url': 'https://example.com/job/123', 'job_title': 'Python工程师'},
    {'job_url': 'https://example.com/job/123', 'job_title': 'Python工程师'},  # ← 重复
])

# ✅ 去重后
df_unique = df.drop_duplicates(subset=['job_url'])

6️⃣ 有效性(Validity)

定义:数据格式是否符合规范。

检测方法

  • 正则表达式验证
  • 枚举值验证

实战案例

python 复制代码
# ❌ 无效的数据
{
    'salary_range': 'abc-xyz',     # ← 非数字
    'education': '研究生以上学历',  # ← 格式不规范
    'email': 'invalid@email'       # ← 邮箱格式错误
}

# ✅ 有效的数据
{
    'salary_range': '20-35K',
    'education': '硕士',
    'email': 'hr@company.com'
}

7️⃣ 可信性(Credibility)

定义:数据来源是否可靠。

检测方法

  • 来源站点信誉度
  • 采集成功率
  • 历史数据对比

🔧 完整性检测:缺失值处理

缺失值的类型

python 复制代码
import pandas as pd
import numpy as np

class MissingValueAnalyzer:
    """
    缺失值分析器
    
    功能:
    1. 检测缺失值的类型(MCAR、MAR、MNAR)
    2. 统计缺失比例
    3. 可视化缺失模式
    """
    
    def __init__(self, df):
        """
        初始化分析器
        
        参数:
            df: pandas DataFrame
        """
        self.df = df
        self.missing_stats = {}
    
    def analyze(self):
        """
        全面分析缺失值
        
        返回:
            {
                'total_missing': int,           # 总缺失数
                'missing_rate': float,          # 总缺失率
                'field_stats': {                # 字段级统计
                    'field_name': {
                        'missing_count': int,
                        'missing_rate': float,
                        'missing_type': str     # MCAR/MAR/MNAR
                    }
                }
            }
        """
        total_cells = self.df.shape[0] * self.df.shape[1]
        total_missing = self.df.isnull().sum().sum()
        
        field_stats = {}
        
        for col in self.df.columns:
            missing_count = self.df[col].isnull().sum()
            missing_rate = (missing_count / len(self.df)) * 100
            
            # 推断缺失类型
            missing_type = self._infer_missing_type(col)
            
            field_stats[col] = {
                'missing_count': missing_count,
                'missing_rate': round(missing_rate, 2),
                'missing_type': missing_type,
                'non_null_count': len(self.df) - missing_count
            }
        
        self.missing_stats = {
            'total_cells': total_cells,
            'total_missing': total_missing,
            'missing_rate': round((total_missing / total_cells) * 100, 2),
            'field_stats': field_stats
        }
        
        return self.missing_stats
    
    def _infer_missing_type(self, column):
        """
        推断缺失值类型
        
        MCAR (Missing Completely At Random): 完全随机缺失
        MAR (Missing At Random): 随机缺失(与其他变量相关)
        MNAR (Missing Not At Random): 非随机缺失(与自身值相关)
        
        简化判断规则:
        - 缺失率 < 5%: 推断为 MCAR
        - 5% <= 缺失率 < 30%: 推断为 MAR
        - 缺失率 >= 30%: 推断为 MNAR
        """
        missing_rate = (self.df[column].isnull().sum() / len(self.df)) * 100
        
        if missing_rate < 5:
            return 'MCAR'
        elif missing_rate < 30:
            return 'MAR'
        else:
            return 'MNAR'
    
    def print_report(self):
        """打印缺失值分析报告"""
        if not self.missing_stats:
            self.analyze()
        
        print("\n" + "="*70)
        print("📊 缺失值分析报告")
        print("="*70)
        print(f"总样本数:     {len(self.df)}")
        print(f"总字段数:     {len(self.df.columns)}")
        print(f"总单元格数:   {self.missing_stats['total_cells']}")
        print(f"总缺失数:     {self.missing_stats['total_missing']}")
        print(f"总缺失率:     {self.missing_stats['missing_rate']}%")
        print("-"*70)
        
        print(f"\n{'字段名':<20} {'缺失数':>10} {'缺失率':>10} {'类型':>10}")
        print("-"*70)
        
        for field, stats in self.missing_stats['field_stats'].items():
            if stats['missing_count'] > 0:
                print(f"{field:<20} {stats['missing_count']:>10} "
                      f"{stats['missing_rate']:>9.2f}% {stats['missing_type']:>10}")
        
        print("="*70 + "\n")

# 使用示例
# 假设我们有 Boss 直聘的职位数据
data = {
    'job_title': ['Python工程师', 'Java工程师', None, '数据分析师'],
    'salary_range': ['20-35K', None, '15-25K', '18-30K'],
    'company_name': ['字节跳动', '阿里巴巴', '腾讯', None],
    'experience': ['3-5年', '5-10年', None, '1-3年'],
    'education': ['本科', None, None, '硕士']
}

df = pd.DataFrame(data)
analyzer = MissingValueAnalyzer(df)
analyzer.print_report()

输出示例:

json 复制代码
======================================================================
📊 缺失值分析报告
======================================================================
总样本数:     4
总字段数:     5
总单元格数:   20
总缺失数:     5
总缺失率:     25.0%
----------------------------------------------------------------------

字段名                   缺失数      缺失率       类型
----------------------------------------------------------------------
job_title                     1      25.00%        MAR
salary_range                  1      25.00%        MAR
company_name                  1      25.00%        MAR
experience                    1      25.00%        MAR
education                     2      50.00%       MNAR
======================================================================

缺失值处理策略

python 复制代码
class MissingValueHandler:
    """
    缺失值处理器
    
    支持 5 种处理策略:
    1. 删除策略(Delete)
    2. 填充策略(Impute)
    3. 推断策略(Predict)
    4. 标记策略(Flag)
    5. 上报策略(Report)
    """
    
    def __init__(self, df):
        self.df = df.copy()
        self.original_df = df.copy()
        self.operations_log = []
    
    # ========== 策略 1:删除策略 ==========
    
    def delete_missing_rows(self, columns=None, threshold=None):
        """
        删除包含缺失值的行
        
        参数:
            columns: 指定列(None 表示所有列)
            threshold: 缺失值阈值(行内缺失字段数 >= threshold 时删除)
        
        示例:
            # 删除 salary_range 字段缺失的行
            handler.delete_missing_rows(columns=['salary_range'])
            
            # 删除缺失字段数 >= 2 的行
            handler.delete_missing_rows(threshold=2)
        """
        before_count = len(self.df)
        
        if threshold:
            # 计算每行缺失值数量
            missing_counts = self.df.isnull().sum(axis=1)
            self.df = self.df[missing_counts < threshold]
        else:
            # 删除指定列缺失的行
            if columns:
                self.df = self.df.dropna(subset=columns)
            else:
                self.df = self.df.dropna()
        
        after_count = len(self.df)
        deleted_count = before_count - after_count
        
        self.operations_log.append({
            'operation': 'delete_rows',
            'columns': columns,
            'threshold': threshold,
            'deleted_count': deleted_count
        })
        
        print(f"✂️ 删除了 {deleted_count} 行({deleted_count/before_count*100:.2f}%)")
        
        return self.df
    
    def delete_missing_columns(self, threshold=0.5):
        """
        删除缺失率过高的列
        
        参数:
            threshold: 缺失率阈值(0.5 表示缺失率 >= 50% 的列被删除)
        
        示例:
            # 删除缺失率 >= 50% 的列
            handler.delete_missing_columns(threshold=0.5)
        """
        before_cols = list(self.df.columns)
        
        # 计算每列缺失率
        missing_rates = self.df.isnull().sum() / len(self.df)
        
        # 找出缺失率过高的列
        cols_to_drop = missing_rates[missing_rates >= threshold].index.tolist()
        
        # 删除列
        self.df = self.df.drop(columns=cols_to_drop)
        
        self.operations_log.append({
            'operation': 'delete_columns',
            'threshold': threshold,
            'deleted_columns': cols_to_drop
        })
        
        print(f"✂️ 删除了 {len(cols_to_drop)} 列: {cols_to_drop}")
        
        return self.df
    
    # ========== 策略 2:填充策略 ==========
    
    def fill_with_constant(self, columns, value):
        """
        用常量填充缺失值
        
        参数:
            columns: 要填充的列
            value: 填充值
        
        示例:
            # 用 '未知' 填充 company_name 的缺失值
            handler.fill_with_constant(['company_name'], '未知')
        """
        for col in columns:
            before_count = self.df[col].isnull().sum()
            self.df[col] = self.df[col].fillna(value)
            after_count = self.df[col].isnull().sum()
            
            filled_count = before_count - after_count
            
            print(f"📝 {col}: 用 '{value}' 填充了 {filled_count} 个缺失值")
        
        self.operations_log.append({
            'operation': 'fill_constant',
            'columns': columns,
            'value': value
        })
        
        return self.df
    
    def fill_with_stats(self, columns, method='mean'):
        """
        用统计值填充缺失值
        
        参数:
            columns: 要填充的列
            method: 统计方法('mean', 'median', 'mode')
        
        示例:
            # 用中位数填充薪资缺失值
            handler.fill_with_stats(['salary_min', 'salary_max'], method='median')
        """
        for col in columns:
            before_count = self.df[col].isnull().sum()
            
            if method == 'mean':
                fill_value = self.df[col].mean()
            elif method == 'median':
                fill_value = self.df[col].median()
            elif method == 'mode':
                fill_value = self.df[col].mode()[0] if not self.df[col].mode().empty else None
            else:
                raise ValueError(f"不支持的方法: {method}")
            
            if fill_value is not None:
                self.df[col] = self.df[col].fillna(fill_value)
                after_count = self.df[col].isnull().sum()
                filled_count = before_count - after_count
                
                print(f"📊 {col}: 用 {method}({fill_value:.2f}) 填充了 {filled_count} 个缺失值")
        
        self.operations_log.append({
            'operation': 'fill_stats',
            'columns': columns,
            'method': method
        })
        
        return self.df
    
    def fill_forward_backward(self, columns, method='ffill'):
        """
        前向/后向填充(适用于时间序列数据)
        
        参数:
            columns: 要填充的列
            method: 'ffill'(前向填充)或 'bfill'(后向填充)
        
        示例:
            # 用前一个值填充
            handler.fill_forward_backward(['price'], method='ffill')
        """
        for col in columns:
            before_count = self.df[col].isnull().sum()
            
            if method == 'ffill':
                self.df[col] = self.df[col].fillna(method='ffill')
            elif method == 'bfill':
                self.df[col] = self.df[col].fillna(method='bfill')
            
            after_count = self.df[col].isnull().sum()
            filled_count = before_count - after_count
            
            print(f"⏩ {col}: 用 {method} 填充了 {filled_count} 个缺失值")
        
        return self.df
    
    # ========== 策略 3:推断策略 ==========
    
    def predict_missing_values(self, target_column, feature_columns, model='knn'):
        """
        使用机器学习模型推断缺失值
        
        参数:
            target_column: 要推断的列
            feature_columns: 用于推断的特征列
            model: 模型类型('knn', 'linear', 'random_forest')
        
        示例:
            # 用 KNN 推断薪资缺失值
            handler.predict_missing_values(
                target_column='salary_avg',
                feature_columns=['experience_years', 'education_level'],
                model='knn'
            )
        """
        from sklearn.impute import KNNImputer
        from sklearn.linear_model import LinearRegression
        from sklearn.ensemble import RandomForestRegressor
        
        # 准备数据
        X = self.df[feature_columns]
        y = self.df[target_column]
        
        # 分离有值和缺失的数据
        mask_missing = y.isnull()
        X_train = X[~mask_missing]
        y_train = y[~mask_missing]
        X_predict = X[mask_missing]
        
        if len(X_predict) == 0:
            print(f"✅ {target_column} 无缺失值,无需推断")
            return self.df
        
        # 选择模型
        if model == 'knn':
            imputer = KNNImputer(n_neighbors=5)
            # KNN 需要所有特征都没有缺失
            combined = pd.concat([X, y], axis=1)
            filled = imputer.fit_transform(combined)
            self.df[target_column] = filled[:, -1]
        
        elif model in ['linear', 'random_forest']:
            # 训练模型
            if model == 'linear':
                ml_model = LinearRegression()
            else:
                ml_model = RandomForestRegressor(n_estimators=100, random_state=42)
            
            ml_model.fit(X_train, y_train)
            
            # 预测缺失值
            y_predicted = ml_model.predict(X_predict)
            
            # 填充
            self.df.loc[mask_missing, target_column] = y_predicted
        
        print(f"🤖 使用 {model} 模型推断了 {mask_missing.sum()} 个 {target_column} 的缺失值")
        
        return self.df
    
    # ========== 策略 4:标记策略 ==========
    
    def flag_missing(self, columns):
        """
        为缺失值创建标记列(用于后续分析缺失模式)
        
        参数:
            columns: 要标记的列
        
        示例:
            # 为 salary_range 创建缺失标记
            handler.flag_missing(['salary_range'])
            # 生成新列: salary_range_missing (True/False)
        """
        for col in columns:
            flag_col = f"{col}_missing"
            self.df[flag_col] = self.df[col].isnull()
            
            missing_count = self.df[flag_col].sum()
            print(f"🏷️ 创建标记列: {flag_col} (标记了 {missing_count} 个缺失值)")
        
        self.operations_log.append({
            'operation': 'flag_missing',
            'columns': columns
        })
        
        return self.df
    
    # ========== 策略 5:上报策略 ==========
    
    def report_missing_to_log(self, output_file='data/missing_report.json'):
        """
        将缺失值详情导出到日志文件(用于人工审核)
        
        参数:
            output_file: 输出文件路径
        """
        import json
        from datetime import datetime
        
        # 找出所有有缺失值的行
        missing_rows = self.df[self.df.isnull().any(axis=1)]
        
        report = {
            'timestamp': datetime.now().isoformat(),
            'total_records': len(self.df),
            'records_with_missing': len(missing_rows),
            'missing_records': missing_rows.to_dict(orient='records')
        }
        
        with open(output_file, 'w', encoding='utf-8') as f:
            json.dump(report, f, ensure_ascii=False, indent=2)
        
        print(f"📄 缺失值报告已导出: {output_file}")
        print(f"   包含 {len(missing_rows)} 条有缺失的记录")
        
        return report
    
    def get_operations_log(self):
        """获取所有处理操作的日志"""
        return self.operations_log

# 使用示例
handler = MissingValueHandler(df)

# 策略 1:删除缺失率过高的列
handler.delete_missing_columns(threshold=0.5)

# 策略 2:填充常量
handler.fill_with_constant(['company_name'], '未知')

# 策略 3:填充统计值
handler.fill_with_stats(['salary_min', 'salary_max'], method='median')

# 策略 4:标记缺失
handler.flag_missing(['education'])

# 策略 5:导出报告
handler.report_missing_to_log()

# 查看处理后的数据
print(handler.df.head())

🚨 异常值检测:统计与机器学习方法

异常值的定义与类型

异常值(Outlier):显著偏离数据集其余部分的观测值。

类型分类:

  1. 点异常:单个数据点异常(如薪资 -100K)
  2. 上下文异常:在特定上下文中异常(如 7月的供暖费用飙升)
  3. 集体异常:一组数据点共同表现异常(如连续 10 天股价相同)

方法 1:统计学方法(3σ原则)

python 复制代码
class StatisticalOutlierDetector:
    """
    统计学异常值检测器
    
    基于 3σ 原则(68-95-99.7 规则):
    - 68% 的数据在 μ ± 1σ 范围内
    - 95% 的数据在 μ ± 2σ 范围内
    - 99.7% 的数据在 μ ± 3σ 范围内
    
    超出 μ ± 3σ 的数据被视为异常值
    """
    
    def __init__(self, df):
        self.df = df.copy()
        self.outliers = {}
    
    def detect_by_zscore(self, column, threshold=3):
        """
        使用 Z-score 检测异常值
        
        参数:
            column: 要检测的列
            threshold: Z-score 阈值(默认 3)
        
        返回:
            异常值的索引列表
        
        公式:
            Z = (X - μ) / σ
            其中 μ 是均值,σ 是标准差
        """
        # 计算 Z-score
        mean = self.df[column].mean()
        std = self.df[column].std()
        
        if std == 0:
            print(f"⚠️ {column} 的标准差为 0,无法检测异常值")
            return []
        
        z_scores = (self.df[column] - mean) / std
        
        # 找出绝对值 > threshold 的点
        outlier_mask = np.abs(z_scores) > threshold
        outlier_indices = self.df[outlier_mask].index.tolist()
        
        self.outliers[column] = {
            'method': 'z-score',
            'threshold': threshold,
            'count': len(outlier_indices),
            'indices': outlier_indices,
            'values': self.df.loc[outlier_indices, column].tolist()
        }
        
        print(f"📊 {column} Z-score 检测:")
        print(f"   均值: {mean:.2f}, 标准差: {std:.2f}")
        print(f"   检测到 {len(outlier_indices)} 个异常值 ({len(outlier_indices)/len(self.df)*100:.2f}%)")
        
        if len(outlier_indices) > 0 and len(outlier_indices) <= 10:
            print(f"   异常值: {self.outliers[column]['values']}")
        
        return outlier_indices
    
    def detect_by_iqr(self, column, k=1.5):
        """
        使用 IQR (四分位距) 方法检测异常值
        
        参数:
            column: 要检测的列
            k: IQR 倍数(默认 1.5)
        
        原理:
            Q1 = 第 25 百分位数
            Q3 = 第 75 百分位数
            IQR = Q3 - Q1
            下界 = Q1 - k * IQR
            上界 = Q3 + k * IQR
            
            超出 [下界, 上界] 的值被视为异常
        """
        Q1 = self.df[column].quantile(0.25)
        Q3 = self.df[column].quantile(0.75)
        IQR = Q3 - Q1
        
        lower_bound = Q1 - k * IQR
        upper_bound = Q3 + k * IQR
        
        # 找出异常值
        outlier_mask = (self.df[column] < lower_bound) | (self.df[column] > upper_bound)
        outlier_indices = self.df[outlier_mask].index.tolist()
        
        self.outliers[column] = {
            'method': 'iqr',
            'k': k,
            'Q1': Q1,
            'Q3': Q3,
            'IQR': IQR,
            'lower_bound': lower_bound,
            'upper_bound': upper_bound,
            'count': len(outlier_indices),
            'indices': outlier_indices,
            'values': self.df.loc[outlier_indices, column].tolist()
        }
        
        print(f"📦 {column} IQR 检测:")
        print(f"   Q1: {Q1:.2f}, Q3: {Q3:.2f}, IQR: {IQR:.2f}")
        print(f"   范围: [{lower_bound:.2f}, {upper_bound:.2f}]")
        print(f"   检测到 {len(outlier_indices)} 个异常值 ({len(outlier_indices)/len(self.df)*100:.2f}%)")
        
        return outlier_indices

# 使用示例:检测Boss直聘薪资数据的异常值
data = {
    'job_title': ['Python工程师']*100,
    'salary_min': np.random.normal(20, 5, 100).tolist() + [-10, 200],  # 添加 2 个异常值
}
df_salary = pd.DataFrame(data)

detector = StatisticalOutlierDetector(df_salary)

# 方法 1:Z-score
outliers_z = detector.detect_by_zscore('salary_min', threshold=3)

# 方法 2:IQR
outliers_iqr = detector.detect_by_iqr('salary_min', k=1.5)

输出示例:

复制代码
📊 salary_min Z-score 检测:
   均值: 21.34, 标准差: 18.12
   检测到 2 个异常值 (1.96%)
   异常值: [-10.0, 200.0]

📦 salary_min IQR 检测:
   Q1: 17.25, Q3: 25.43, IQR: 8.18
   范围: [4.98, 37.70]
   检测到 2 个异常值 (1.96%)

方法 2:箱线图法(可视化)

python 复制代码
import matplotlib.pyplot as plt

class BoxPlotDetector:
    """
    箱线图异常值检测
    
    优势:直观可视化
    """
    
    def detect_and_visualize(self, df, column):
        """
        绘制箱线图并标记异常值
        
        参数:
            df: DataFrame
            column: 列名
        """
        fig, ax = plt.subplots(figsize=(10, 6))
        
        # 绘制箱线图
        bp = ax.boxplot(df[column].dropna(), vert=False, patch_artist=True)
        
        # 美化
        bp['boxes'][0].set_facecolor('lightblue')
        bp['boxes'][0].set_alpha(0.7)
        
        # 计算统计值
        Q1 = df[column].quantile(0.25)
        Q3 = df[column].quantile(0.75)
        IQR = Q3 - Q1
        lower_bound = Q1 - 1.5 * IQR
        upper_bound = Q3 + 1.5 * IQR
        
        # 标注统计信息
        ax.axvline(Q1, color='green', linestyle='--', label=f'Q1 ({Q1:.2f})')
        ax.axvline(Q3, color='green', linestyle='--', label=f'Q3 ({Q3:.2f})')
        ax.axvline(lower_bound, color='red', linestyle='--', label=f'下界 ({lower_bound:.2f})')
        ax.axvline(upper_bound, color='red', linestyle='--', label=f'上界 ({upper_bound:.2f})')
        
        ax.set_xlabel(column)
        ax.set_title(f'{column} 箱线图异常值检测')
        ax.legend()
        ax.grid(True, alpha=0.3)
        
        plt.tight_layout()
        plt.savefig(f'data/{column}_boxplot.png', dpi=150)
        print(f"📊 箱线图已保存: data/{column}_boxplot.png")
        
        # 返回异常值
        outlier_mask = (df[column] < lower_bound) | (df[column] > upper_bound)
        return df[outlier_mask].index.tolist()

方法 3:孤立森林(Isolation Forest)

python 复制代码
from sklearn.ensemble import IsolationForest

class IsolationForestDetector:
    """
    孤立森林异常检测
    
    原理:
    异常点更容易被隔离(需要更少的随机切分)
    
    优势:
    - 无需假设数据分布
    - 支持多维特征
    - 对高维数据有效
    """
    
    def __init__(self, contamination=0.05):
        """
        初始化检测器
        
        参数:
            contamination: 预期异常值比例(默认 5%)
        """
        self.model = IsolationForest(
            contamination=contamination,
            random_state=42,
            n_estimators=100
        )
        self.contamination = contamination
    
    def detect(self, df, features):
        """
        检测异常值
        
        参数:
            df: DataFrame
            features: 用于检测的特征列表
        
        返回:
            异常值索引列表
        """
        # 准备数据
        X = df[features].fillna(0)  # 简单填充缺失值
        
        # 训练并预测
        predictions = self.model.fit_predict(X)
        
        # -1 表示异常,1 表示正常
        outlier_mask = predictions == -1
        outlier_indices = df[outlier_mask].index.tolist()
        
        print(f"🌲 孤立森林检测 (contamination={self.contamination}):")
        print(f"   特征: {features}")
        print(f"   检测到 {len(outlier_indices)} 个异常值 ({len(outlier_indices)/len(df)*100:.2f}%)")
        
        # 获取异常分数(分数越低越异常)
        scores = self.model.score_samples(X)
        df_result = df.copy()
        df_result['anomaly_score'] = scores
        df_result['is_outlier'] = outlier_mask
        
        return outlier_indices, df_result

# 使用示例:多维异常检测
data = {
    'salary_min': np.random.normal(20, 5, 100).tolist() + [200, -10],
    'experience_years': np.random.randint(1, 10, 100).tolist() + [50, 0],
    'company_scale': np.random.randint(100, 10000, 100).tolist() + [100000, 1]
}
df_multi = pd.DataFrame(data)

detector_if = IsolationForestDetector(contamination=0.05)
outliers, df_with_scores = detector_if.detect(df_multi, ['salary_min', 'experience_years', 'company_scale'])

# 查看异常值详情
print("\n异常值详情:")
print(df_with_scores[df_with_scores['is_outlier']].head())

方法 4:DBSCAN 聚类

python 复制代码
from sklearn.cluster import DBSCAN
from sklearn.preprocessing import StandardScaler

class DBSCANDetector:
    """
    DBSCAN 密度聚类异常检测
    
    原理:
    密度低的点(不属于任何簇)被视为异常
    
    优势:
    - 无需预设簇数量
    - 能检测任意形状的簇
    - 对噪声鲁棒
    """
    
    def __init__(self, eps=0.5, min_samples=5):
        """
        初始化检测器
        
        参数:
            eps: 邻域半径
            min_samples: 最小样本数
        """
        self.eps = eps
        self.min_samples = min_samples
        self.scaler = StandardScaler()
    
    def detect(self, df, features):
        """
        检测异常值
        
        参数:
            df: DataFrame
            features: 特征列表
        
        返回:
            异常值索引列表
        """
        # 数据标准化(重要!DBSCAN 对尺度敏感)
        X = df[features].fillna(0)
        X_scaled = self.scaler.fit_transform(X)
        
        # 聚类
        dbscan = DBSCAN(eps=self.eps, min_samples=self.min_samples)
        clusters = dbscan.fit_predict(X_scaled)
        
        # -1 表示噪声点(异常)
        outlier_mask = clusters == -1
        outlier_indices = df[outlier_mask].index.tolist()
        
        print(f"🔬 DBSCAN 检测 (eps={self.eps}, min_samples={self.min_samples}):")
        print(f"   特征: {features}")
        print(f"   簇数量: {len(set(clusters)) - (1 if -1 in clusters else 0)}")
        print(f"   检测到 {len(outlier_indices)} 个异常值 ({len(outlier_indices)/len(df)*100:.2f}%)")
        
        df_result = df.copy()
        df_result['cluster'] = clusters
        df_result['is_outlier'] = outlier_mask
        
        return outlier_indices, df_result

# 使用示例
detector_db = DBSCANDetector(eps=0.5, min_samples=5)
outliers_db, df_clusters = detector_db.detect(df_multi, ['salary_min', 'experience_years'])

🎯 业务规则检测:Boss直聘特定场景

薪资异常检测

python 复制代码
class SalaryAnomalyDetector:
    """
    薪资数据异常检测器
    
    检测规则:
    1. 薪资为负数
    2. 最低薪资 > 最高薪资
    3. 薪资范围过大(如 10-100K,不合理)
    4. 薪资突变(同一职位薪资前后差距过大)
    5. 薪资越界(超出合理范围,如 > 200K for 初级岗位)
    """
    
    def __init__(self, df):
        self.df = df.copy()
        self.anomalies = []
    
    def parse_salary_range(self, salary_str):
        """
        解析薪资字符串
        
        输入:'20-35K'
        输出:(20, 35, 'K')
        """
        import re
        
        if pd.isna(salary_str) or salary_str == '':
            return None, None, None
        
        # 匹配模式:数字-数字K/k
        pattern = r'(\d+)-(\d+)([KkMm]?)'
        match = re.search(pattern, str(salary_str))
        
        if match:
            min_sal = float(match.group(1))
            max_sal = float(match.group(2))
            unit = match.group(3).upper() if match.group(3) else 'K'
            
            # 统一单位为 K
            if unit == 'M':
                min_sal *= 1000
                max_sal *= 1000
            
            return min_sal, max_sal, unit
        
        return None, None, None
    
    def detect_all(self):
        """执行所有薪资异常检测"""
        
        # 解析薪资范围
        self.df[['salary_min', 'salary_max', 'salary_unit']] = self.df['salary_range'].apply(
            lambda x: pd.Series(self.parse_salary_range(x))
        )
        
        # 规则 1:负数检测
        self._detect_negative_salary()
        
        # 规则 2:逻辑错误检测
        self._detect_logic_error()
        
        # 规则 3:范围异常检测
        self._detect_range_anomaly()
        
        # 规则 4:突变检测
        self._detect_sudden_change()
        
        # 规则 5:越界检测
        self._detect_out_of_range()
        
        return self.anomalies
    
    def _detect_negative_salary(self):
        """检测负数薪资"""
        mask = (self.df['salary_min'] < 0) | (self.df['salary_max'] < 0)
        
        if mask.sum() > 0:
            anomaly = {
                'rule': '负数薪资',
                'severity': 'critical',  # 严重程度
                'count': mask.sum(),
                'indices': self.df[mask].index.tolist(),
                'samples': self.df[mask][['job_title', 'salary_range']].head(5).to_dict('records')
            }
            self.anomalies.append(anomaly)
            print(f"🚨 检测到 {mask.sum()} 条负数薪资记录!")
    
    def _detect_logic_error(self):
        """检测最小值 > 最大值"""
        mask = self.df['salary_min'] > self.df['salary_max']
        
        if mask.sum() > 0:
            anomaly = {
                'rule': '逻辑错误(min > max)',
                'severity': 'critical',
                'count': mask.sum(),
                'indices': self.df[mask].index.tolist(),
                'samples': self.df[mask][['job_title', 'salary_range']].head(5).to_dict('records')
            }
            self.anomalies.append(anomaly)
            print(f"⚠️ 检测到 {mask.sum()} 条逻辑错误记录!")
    
    def _detect_range_anomaly(self):
        """检测薪资范围异常(跨度过大)"""
        self.df['salary_range_span'] = self.df['salary_max'] - self.df['salary_min']
        
        # 规则:范围 > 平均范围的 3 倍
        mean_span = self.df['salary_range_span'].mean()
        threshold = mean_span * 3
        
        mask = self.df['salary_range_span'] > threshold
        
        if mask.sum() > 0:
            anomaly = {
                'rule': '薪资范围异常',
                'severity': 'warning',
                'threshold': threshold,
                'count': mask.sum(),
                'indices': self.df[mask].index.tolist(),
                'samples': self.df[mask][['job_title', 'salary_range', 'salary_range_span']].head(5).to_dict('records')
            }
            self.anomalies.append(anomaly)
            print(f"⚠️ 检测到 {mask.sum()} 条薪资范围异常记录(跨度 > {threshold:.2f}K)!")
    
    def _detect_sudden_change(self):
        """检测同一职位的薪资突变"""
        if 'job_title' not in self.df.columns:
            return
        
        # 按职位分组,计算薪资中位数
        self.df['salary_avg'] = (self.df['salary_min'] + self.df['salary_max']) / 2
        
        sudden_changes = []
        
        for job in self.df['job_title'].unique():
            job_data = self.df[self.df['job_title'] == job]
            
            if len(job_data) < 2:
                continue
            
            median_salary = job_data['salary_avg'].median()
            
            # 检测偏离中位数 > 50% 的记录
            threshold = median_salary * 0.5
            mask = np.abs(job_data['salary_avg'] - median_salary) > threshold
            
            if mask.sum() > 0:
                sudden_changes.extend(job_data[mask].index.tolist())
        
        if len(sudden_changes) > 0:
            anomaly = {
                'rule': '薪资突变',
                'severity': 'warning',
                'count': len(sudden_changes),
                'indices': sudden_changes,
                'samples': self.df.loc[sudden_changes, ['job_title', 'salary_range', 'salary_avg']].head(5).to_dict('records')
            }
            self.anomalies.append(anomaly)
            print(f"⚠️ 检测到 {len(sudden_changes)} 条薪资突变记录!")
    
    def _detect_out_of_range(self):
        """检测薪资越界(基于经验等级)"""
        if 'experience' not in self.df.columns:
            return
        
        # 定义合理范围(简化版)
        salary_ranges = {
            '应届生': (5, 15),
            '1年以下': (5, 15),
            '1-3年': (10, 25),
            '3-5年': (15, 40),
            '5-10年': (25, 60),
            '10年以上': (35, 100)
        }
        
        out_of_range_indices = []
        
        for idx, row in self.df.iterrows():
            exp = row.get('experience', '')
            sal_min = row.get('salary_min', 0)
            sal_max = row.get('salary_max', 0)
            
            # 查找匹配的经验范围
            for exp_key, (min_bound, max_bound) in salary_ranges.items():
                if exp_key in str(exp):
                    # 检查是否越界
                    if sal_min < min_bound or sal_max > max_bound:
                        out_of_range_indices.append(idx)
                    break
        
        if len(out_of_range_indices) > 0:
            anomaly = {
                'rule': '薪资越界',
                'severity': 'warning',
                'count': len(out_of_range_indices),
                'indices': out_of_range_indices,
                'samples': self.df.loc[out_of_range_indices, ['job_title', 'experience', 'salary_range']].head(5).to_dict('records')
            }
            self.anomalies.append(anomaly)
            print(f"⚠️ 检测到 {len(out_of_range_indices)} 条薪资越界记录!")
    
    def generate_report(self, output_file='data/salary_anomaly_report.json'):
        """生成异常检测报告"""
        import json
        from datetime import datetime
        
        report = {
            'timestamp': datetime.now().isoformat(),
            'total_records': len(self.df),
            'total_anomalies': sum(a['count'] for a in self.anomalies),
            'anomalies_by_rule': self.anomalies
        }
        
        with open(output_file, 'w', encoding='utf-8') as f:
            json.dump(report, f, ensure_ascii=False, indent=2)
        
        print(f"\n📄 异常检测报告已生成: {output_file}")
        
        return report

# 使用示例
data_salary = {
    'job_title': ['Python工程师', 'Python工程师', 'Java工程师', 'Python工程师'],
    'salary_range': ['20-35K', '-10-20K', '100-50K', '10-100K'],
    'experience': ['3-5年', '1-3年', '5-10年', '1-3年']
}
df_salary_test = pd.DataFrame(data_salary)

detector_salary = SalaryAnomalyDetector(df_salary_test)
anomalies = detector_salary.detect_all()
report = detector_salary.generate_report()

输出示例:

json 复制代码
🚨 检测到 1 条负数薪资记录!
⚠️ 检测到 1 条逻辑错误记录!
⚠️ 检测到 1 条薪资范围异常记录(跨度 > 45.00K)!
⚠️ 检测到 1 条薪资越界记录!

📄 异常检测报告已生成: data/salary_anomaly_report.json

📊 数据质量监控看板

DQI (Data Quality Index) 指标体系

python 复制代码
class DataQualityMonitor:
    """
    数据质量监控器
    
    计算综合质量指数(DQI):
    DQI = w1*完整性 + w2*准确性 + w3*一致性 + w4*时效性 + w5*唯一性
    
    其中 w1, w2, w3, w4, w5 是权重(总和为 1)
    """
    
    def __init__(self, df, weights=None):
        """
        初始化监控器
        
        参数:
            df: DataFrame
            weights: 权重字典 {'completeness': 0.3, 'accuracy': 0.3, ...}
        """
        self.df = df
        
        # 默认权重
        self.weights = weights or {
            'completeness': 0.25,   # 完整性
            'accuracy': 0.25,       # 准确性
            'consistency': 0.20,    # 一致性
            'timeliness': 0.15,     # 时效性
            'uniqueness': 0.15      # 唯一性
        }
        
        self.metrics = {}
    
    def calculate_completeness(self, required_fields):
        """
        计算完整性分数
        
        参数:
            required_fields: 必填字段列表
        
        返回:
            完整性分数(0-100)
        """
        total_cells = len(self.df) * len(required_fields)
        missing_cells = self.df[required_fields].isnull().sum().sum()
        
        score = ((total_cells - missing_cells) / total_cells) * 100
        
        self.metrics['completeness'] = {
            'score': round(score, 2),
            'total_cells': total_cells,
            'missing_cells': missing_cells,
            'missing_rate': round((missing_cells / total_cells) * 100, 2)
        }
        
        return score
    
    def calculate_accuracy(self, validation_rules):
        """
        计算准确性分数
        
        参数:
            validation_rules: 验证规则列表
                [
                    {'column': 'salary_min', 'rule': lambda x: x > 0, 'name': '薪资为正'},
                    {'column': 'age', 'rule': lambda x: 18 <= x <= 65, 'name': '年龄合理'},
                ]
        
        返回:
            准确性分数(0-100)
        """
        total_records = len(self.df)
        invalid_records = set()
        
        rule_results = []
        
        for rule in validation_rules:
            column = rule['column']
            rule_func = rule['rule']
            rule_name = rule['name']
            
            if column not in self.df.columns:
                continue
            
            # 应用规则
            valid_mask = self.df[column].apply(lambda x: rule_func(x) if pd.notna(x) else False)
            invalid_count = (~valid_mask).sum()
            
            # 收集无效记录索引
            invalid_indices = self.df[~valid_mask].index.tolist()
            invalid_records.update(invalid_indices)
            
            rule_results.append({
                'rule_name': rule_name,
                'column': column,
                'invalid_count': invalid_count,
                'invalid_rate': round((invalid_count / total_records) * 100, 2)
            })
        
        # 计算分数
        score = ((total_records - len(invalid_records)) / total_records) * 100
        
        self.metrics['accuracy'] = {
            'score': round(score, 2),
            'total_records': total_records,
            'invalid_records': len(invalid_records),
            'rules_checked': len(validation_rules),
            'rule_results': rule_results
        }
        
        return score
    
    def calculate_consistency(self, consistency_checks):
        """
        计算一致性分数
        
        参数:
            consistency_checks: 一致性检查列表
                [
                    {
                        'type': 'format',
                        'column': 'company_name',
                        'pattern': r'^[\u4e00-\u9fa5a-zA-Z0-9]+$'  # 只允许中英文数字
                    },
                    {
                        'type': 'cross_field',
                        'check': lambda row: row['salary_min'] <= row['salary_max']
                    }
                ]
        
        返回:
            一致性分数(0-100)
        """
        total_records = len(self.df)
        inconsistent_records = set()
        
        check_results = []
        
        for check in consistency_checks:
            check_type = check['type']
            
            if check_type == 'format':
                column = check['column']
                pattern = check['pattern']
                
                if column not in self.df.columns:
                    continue
                
                import re
                consistent_mask = self.df[column].apply(
                    lambda x: bool(re.match(pattern, str(x))) if pd.notna(x) else False
                )
                
                inconsistent_count = (~consistent_mask).sum()
                inconsistent_indices = self.df[~consistent_mask].index.tolist()
                inconsistent_records.update(inconsistent_indices)
                
                check_results.append({
                    'type': 'format',
                    'column': column,
                    'inconsistent_count': inconsistent_count
                })
            
            elif check_type == 'cross_field':
                check_func = check['check']
                consistent_mask = self.df.apply(check_func, axis=1)
                
                inconsistent_count = (~consistent_mask).sum()
                inconsistent_indices = self.df[~consistent_mask].index.tolist()
                inconsistent_records.update(inconsistent_indices)
                
                check_results.append({
                    'type': 'cross_field',
                    'inconsistent_count': inconsistent_count
                })
        
        # 计算分数
        score = ((total_records - len(inconsistent_records)) / total_records) * 100
        
        self.metrics['consistency'] = {
            'score': round(score, 2),
            'total_records': total_records,
            'inconsistent_records': len(inconsistent_records),
            'checks_performed': len(consistency_checks),
            'check_results': check_results
        }
        
        return score
    
    def calculate_timeliness(self, timestamp_column, max_age_days=7):
        """
        计算时效性分数
        
        参数:
            timestamp_column: 时间戳列名
            max_age_days: 最大允许天数
        
        返回:
            时效性分数(0-100)
        """
        from datetime import datetime, timedelta
        
        if timestamp_column not in self.df.columns:
            return 0
        
        now = datetime.now()
        threshold = now - timedelta(days=max_age_days)
        
        # 转换时间戳
        self.df[timestamp_column] = pd.to_datetime(self.df[timestamp_column])
        
        # 计算过时数据
        outdated_mask = self.df[timestamp_column] < threshold
        outdated_count = outdated_mask.sum()
        
        score = ((len(self.df) - outdated_count) / len(self.df)) * 100
        
        self.metrics['timeliness'] = {
            'score': round(score, 2),
            'total_records': len(self.df),
            'outdated_records': outdated_count,
            'max_age_days': max_age_days,
            'threshold_date': threshold.strftime('%Y-%m-%d')
        }
        
        return score
    
    def calculate_uniqueness(self, key_columns):
        """
        计算唯一性分数
        
        参数:
            key_columns: 用于去重的键列
        
        返回:
            唯一性分数(0-100)
        """
        total_records = len(self.df)
        unique_records = self.df.drop_duplicates(subset=key_columns, keep='first')
        duplicate_count = total_records - len(unique_records)
        
        score = (len(unique_records) / total_records) * 100
        
        self.metrics['uniqueness'] = {
            'score': round(score, 2),
            'total_records': total_records,
            'unique_records': len(unique_records),
            'duplicate_count': duplicate_count,
            'duplicate_rate': round((duplicate_count / total_records) * 100, 2)
        }
        
        return score
    
    def calculate_dqi(self):
        """
        计算综合数据质量指数 (DQI)
        
        返回:
            DQI 分数(0-100)
        """
        dqi = 0
        
        for dimension, weight in self.weights.items():
            if dimension in self.metrics:
                dqi += self.metrics[dimension]['score'] * weight
        
        return round(dqi, 2)
    
    def generate_dashboard(self, output_file='data/dqi_dashboard.json'):
        """生成质量看板"""
        import json
        from datetime import datetime
        
        dqi = self.calculate_dqi()
        
        dashboard = {
            'timestamp': datetime.now().isoformat(),
            'dqi': dqi,
            'grade': self._get_grade(dqi),
            'weights': self.weights,
            'metrics': self.metrics,
            'recommendations': self._generate_recommendations()
        }
        
        with open(output_file, 'w', encoding='utf-8') as f:
            json.dump(dashboard, f, ensure_ascii=False, indent=2)
        
        print(f"\n📊 数据质量看板已生成: {output_file}")
        self._print_dashboard(dashboard)
        
        return dashboard
    
    def _get_grade(self, dqi):
        """根据 DQI 分数评级"""
        if dqi >= 90:
            return 'A (优秀)'
        elif dqi >= 80:
            return 'B (良好)'
        elif dqi >= 70:
            return 'C (中等)'
        elif dqi >= 60:
            return 'D (及格)'
        else:
            return 'F (不及格)'
    
    def _generate_recommendations(self):
        """生成改进建议"""
        recommendations = []
        
        for dimension, metric in self.metrics.items():
            score = metric['score']
            
            if score < 80:
                if dimension == 'completeness':
                    recommendations.append(f"完整性较低({score}%),建议检查数据采集流程,确保必填字段完整")
                elif dimension == 'accuracy':
                    recommendations.append(f"准确性较低({score}%),建议增强数据验证规则")
                elif dimension == 'consistency':
                    recommendations.append(f"一致性较低({score}%),建议标准化数据格式")
                elif dimension == 'timeliness':
                    recommendations.append(f"时效性较低({score}%),建议增加爬取频率")
                elif dimension == 'uniqueness':
                    recommendations.append(f"唯一性较低({score}%),建议优化去重策略")
        
        return recommendations
    
    def _print_dashboard(self, dashboard):
        """打印看板"""
        print("\n" + "="*70)
        print("📊 数据质量监控看板")
        print("="*70)
        print(f"DQI 综合得分: {dashboard['dqi']} / 100")
        print(f"质量评级:     {dashboard['grade']}")
        print("-"*70)
        
        print("\n各维度得分:")
        for dimension, metric in dashboard['metrics'].items():
            print(f"  {dimension:<15} {metric['score']:>6.2f}%")
        
        print("\n改进建议:")
        for i, rec in enumerate(dashboard['recommendations'], 1):
            print(f"  {i}. {rec}")
        
        print("="*70 + "\n")

# 使用示例
monitor = DataQualityMonitor(df_salary_test)

# 计算各维度分数
monitor.calculate_completeness(required_fields=['job_title', 'salary_range'])
monitor.calculate_accuracy(validation_rules=[
    {'column': 'salary_min', 'rule': lambda x: x > 0, 'name': '薪资为正'},
])
monitor.calculate_consistency(consistency_checks=[
    {
        'type': 'cross_field',
        'check': lambda row: row.get('salary_min', 0) <= row.get('salary_max', 0)
    }
])
monitor.calculate_uniqueness(key_columns=['job_title', 'company_name'])

# 生成看板
dashboard = monitor.generate_dashboard()

输出示例:

json 复制代码
📊 数据质量监控看板已生成: data/dqi_dashboard.json

======================================================================
📊 数据质量监控看板
======================================================================
DQI 综合得分: 72.5 / 100
质量评级:     C (中等)
----------------------------------------------------------------------

各维度得分:
  completeness     100.00%
  accuracy          50.00%
  consistency       75.00%
  uniqueness       100.00%

改进建议:
  1. 准确性较低(50.0%),建议增强数据验证规则
  2. 一致性较低(75.0%),建议标准化数据格式
======================================================================

💡 最佳实践总结

1. 数据质量检测的实施流程

json 复制代码
采集阶段 → 实时验证 → 缓存/存储 → 离线审计 → 人工复核
   ↓           ↓           ↓           ↓           ↓
 格式检查   业务规则    统计检测    ML检测      修正入库

2. 异常值处理策略选择

场景 推荐方法 原因
单维数值型 Z-score / IQR 简单高效
多维数值型 孤立森林 支持高维,无需假设分布
密度聚类场景 DBSCAN 能检测任意形状的异常簇
业务规则明确 规则引擎 可解释性强

3. 性能优化建议

  • ✅ 使用向量化操作(pandas/numpy)而非循环
  • ✅ 采样检测:大数据集先抽样 10% 快速检测
  • ✅ 增量检测:只检测新增数据
  • ✅ 并行处理:多进程处理大文件

📚 本章总结

本章系统讲解了爬虫数据质量检测的完整体系:

7个质量维度 :完整性、准确性、一致性、时效性、唯一性、有效性、可信性

缺失值处理 :5种策略(删除、填充、推断、标记、上报)

异常值检测 :6种方法(Z-score、IQR、箱线图、孤立森林、DBSCAN、业务规则)

质量监控:DQI 指标体系、实时看板、改进建议

关键要点

  1. 数据质量是爬虫项目的生命线:脏数据会毁掉后续所有工作
  2. 预防胜于治疗:在采集阶段就做好验证,而非事后清洗
  3. 多维度综合评估:单一指标容易误导,需要综合 DQI
  4. 持续监控改进:建立质量看板,定期审计

希望这一章能帮你构建生产级的数据质量保障体系!

🌟 文末

好啦~以上就是本期的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥

✅ 专栏持续更新中|建议收藏 + 订阅

墙裂推荐订阅专栏 👉 《Python爬虫实战》,本专栏秉承着以"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一期内容都做到:

✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)

📣 想系统提升的小伙伴 :强烈建议先订阅专栏 《Python爬虫实战》,再按目录大纲顺序学习,效率十倍上升~

✅ 互动征集

想让我把【某站点/某反爬/某验证码/某分布式方案】等写成某期实战?

评论区留言告诉我你的需求,我会优先安排实现(更新)哒~


⭐️ 若喜欢我,就请关注我叭~(更新不迷路)

⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)

⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)


✅ 免责声明

本文爬虫思路、相关技术和代码仅用于学习参考,对阅读本文后的进行爬虫行为的用户本作者不承担任何法律责任。

使用或者参考本项目即表示您已阅读并同意以下条款:

  • 合法使用: 不得将本项目用于任何违法、违规或侵犯他人权益的行为,包括但不限于网络攻击、诈骗、绕过身份验证、未经授权的数据抓取等。
  • 风险自负: 任何因使用本项目而产生的法律责任、技术风险或经济损失,由使用者自行承担,项目作者不承担任何形式的责任。
  • 禁止滥用: 不得将本项目用于违法牟利、黑产活动或其他不当商业用途。
  • 使用或者参考本项目即视为同意上述条款,即 "谁使用,谁负责" 。如不同意,请立即停止使用并删除本项目。!!!
相关推荐
小雨中_2 小时前
2.8 策略梯度(Policy Gradient)算法 与 Actor-critic算法
人工智能·python·深度学习·算法·机器学习
C系语言2 小时前
Anaconda向另外一台电脑打包虚拟环境
python
张3蜂3 小时前
Python 中的 Conda 详解:它到底解决了什么问题?
开发语言·python·conda
清水白石00812 小时前
Python 纯函数编程:从理念到实战的完整指南
开发语言·python
twilight_46913 小时前
机器学习与模式识别——机器学习中的搜索算法
人工智能·python·机器学习
Jia ming13 小时前
《智能法官软件项目》—罪名初判模块
python·教学·案例·智能法官
Jia ming13 小时前
《智能法官软件项目》—法律文书生成模块
python·教学·案例·智能法官软件
曦月逸霜14 小时前
Python数据分析——个人笔记(持续更新中~)
python
海棠AI实验室14 小时前
第六章 从“能用”到“能交付”的关键一刀:偏好对齐(Preference Alignment)数据工程
python·私有模型训练