一、项目背景
1.1 痛点分析
数据清洗占数据分析工作的60%-80%,但传统方式效率极低:
| 环节 | 手工方式 | 时间 |
|---|---|---|
| 空值处理 | 逐行检查 | 1小时 |
| 重复数据 | 排序比对 | 1小时 |
| 格式统一 | 查找替换 | 2小时 |
| 异常值检测 | 肉眼判断 | 2小时 |
| 数据验证 | 抽样核对 | 2小时 |
| 总计 | - | 8小时 |
10万行数据清洗一天,100万行就是一周。
1.2 技术需求
核心需求:
- 自动扫描数据质量问题
- 智能处理空值(删除/填充/预测)
- 自动去重(精确匹配+模糊匹配)
- 格式自动统一(日期/手机号/金额/地址)
- 异常值智能检测与处理
- 生成数据质量报告
二、技术架构
原始数据 → 质量扫描 → 空值处理 → 去重 → 格式统一 → 异常检测 → 验证输出
↑ ↑ ↑ ↑ ↑ ↑ ↑
pandas pandas pandas pandas regex/AI scipy pandas
sklearn DashScope
技术栈:
- **pandas**:数据读取和处理
- **numpy**:数值计算
- **scipy**:统计分析和异常检测
- **sklearn**:机器学习填充缺失值
- **DashScope/Qwen3**:AI智能识别和格式纠正
- **re**:正则表达式格式匹配
---
## 三、环境准备
### 3.1 安装依赖
```bash
pip install pandas numpy scipy scikit-learn dashscope openpyxl
3.2 配置
# config.py
DASHSCOPE_API_KEY = "your-api-key-here"
# 清洗规则配置
CLEAN_CONFIG = {
'date_formats': ['%Y-%m-%d', '%Y/%m/%d', '%Y年%m月%d日', '%Y%m%d'],
'phone_pattern': r'^1[3-9]\d{9}$',
'email_pattern': r'^[\w.-]+@[\w.-]+\.\w+$',
'outlier_method': 'iqr', # iqr / zscore / isolation_forest
'outlier_threshold': 1.5
}
四、核心模块实现
4.1 数据质量扫描模块
自动扫描数据质量问题,生成健康报告:
import pandas as pd
import numpy as np
class DataProfiler:
def __init__(self, df):
self.df = df
self.report = {}
def scan(self):
"""全面扫描数据质量"""
self.report = {
'基本信息': self._basic_info(),
'空值统计': self._null_analysis(),
'重复统计': self._duplicate_analysis(),
'类型检测': self._type_detection(),
'格式问题': self._format_issues(),
'异常值预检': self._outlier_preview(),
'质量评分': 0
}
# 计算质量评分(0-100)
self.report['质量评分'] = self._calculate_score()
return self.report
def _basic_info(self):
return {
'总行数': len(self.df),
'总列数': len(self.df.columns),
'数值列': list(self.df.select_dtypes(include=[np.number]).columns),
'文本列': list(self.df.select_dtypes(include=['object']).columns),
'内存占用': f"{self.df.memory_usage(deep=True).sum() / 1024 / 1024:.2f}MB"
}
def _null_analysis(self):
null_stats = {}
for col in self.df.columns:
null_count = self.df[col].isnull().sum()
null_pct = round(null_count / len(self.df) * 100, 2)
null_stats[col] = {
'空值数': int(null_count),
'空值率': f"{null_pct}%",
'建议': self._null_suggestion(null_pct, self.df[col].dtype)
}
return null_stats
def _null_suggestion(self, null_pct, dtype):
if null_pct == 0:
return '无需处理'
elif null_pct < 5:
return '删除空值行' if dtype == 'object' else '中位数填充'
elif null_pct < 30:
return 'KNN填充' if dtype != 'object' else '众数填充'
else:
return '考虑删除该列'
def _duplicate_analysis(self):
dup_count = self.df.duplicated().sum()
return {
'完全重复行': int(dup_count),
'重复率': f"{round(dup_count / len(self.df) * 100, 2)}%"
}
def _type_detection(self):
"""检测数据类型是否合理"""
issues = {}
for col in self.df.columns:
if self.df[col].dtype == 'object':
# 检测是否应该是数值
numeric_count = self.df[col].apply(
lambda x: str(x).replace('.', '').replace('-', '').isdigit()
if pd.notna(x) else False
).sum()
if numeric_count / len(self.df) > 0.8:
issues[col] = '疑似数值列被识别为文本'
return issues
def _format_issues(self):
"""检测格式不统一问题"""
issues = {}
for col in self.df.columns:
if self.df[col].dtype == 'object':
unique_patterns = self.df[col].dropna().apply(
lambda x: self._get_pattern(str(x))
).nunique()
if unique_patterns > 3:
issues[col] = f"发现{unique_patterns}种格式"
return issues
def _get_pattern(self, text):
import re
pattern = re.sub(r'\d', 'D', text)
pattern = re.sub(r'[a-zA-Z]', 'A', pattern)
pattern = re.sub(r'[\u4e00-\u9fff]', 'C', pattern)
return pattern
def _outlier_preview(self):
"""异常值预检"""
outliers = {}
for col in self.df.select_dtypes(include=[np.number]).columns:
Q1 = self.df[col].quantile(0.25)
Q3 = self.df[col].quantile(0.75)
IQR = Q3 - Q1
count = ((self.df[col] < Q1 - 1.5 * IQR) | (self.df[col] > Q3 + 1.5 * IQR)).sum()
if count > 0:
outliers[col] = int(count)
return outliers
def _calculate_score(self):
"""计算质量评分"""
score = 100
# 空值扣分
total_null = self.df.isnull().sum().sum()
null_rate = total_null / (len(self.df) * len(self.df.columns))
score -= null_rate * 100 * 2
# 重复扣分
dup_rate = self.df.duplicated().sum() / len(self.df)
score -= dup_rate * 100
# 异常值扣分
outlier_count = sum(self.report['异常值预检'].values()) if self.report['异常值预检'] else 0
outlier_rate = outlier_count / len(self.df)
score -= outlier_rate * 50
return max(0, round(score, 1))
4.2 智能空值处理模块
根据数据特征自动选择最佳填充策略:
from sklearn.impute import KNNImputer
class NullHandler:
def __init__(self):
self.strategies = {}
def handle(self, df, strategy='auto'):
"""智能处理空值"""
if strategy == 'auto':
return self._auto_handle(df)
elif strategy == 'drop':
return df.dropna()
elif strategy == 'fill_median':
return df.fillna(df.median(numeric_only=True))
def _auto_handle(self, df):
"""自动选择最佳策略"""
for col in df.columns:
null_pct = df[col].isnull().sum() / len(df) * 100
if null_pct == 0:
continue
if null_pct > 50:
# 空值超过50%,考虑删除列
self.strategies[col] = '删除列(空值过多)'
df = df.drop(columns=[col])
continue
if df[col].dtype in ['int64', 'float64']:
if null_pct < 5:
# 少量空值用中位数
df[col] = df[col].fillna(df[col].median())
self.strategies[col] = '中位数填充'
else:
# 较多空值用KNN
df = self._knn_fill(df, col)
self.strategies[col] = 'KNN填充'
else:
# 文本列用众数
mode_val = df[col].mode()
if len(mode_val) > 0:
df[col] = df[col].fillna(mode_val[0])
self.strategies[col] = '众数填充'
else:
df[col] = df[col].fillna('未知')
self.strategies[col] = '默认值填充'
return df
def _knn_fill(self, df, target_col):
"""KNN填充缺失值"""
numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
if target_col not in numeric_cols:
return df
imputer = KNNImputer(n_neighbors=5)
df[numeric_cols] = imputer.fit_transform(df[numeric_cols])
return df
def get_strategies(self):
return self.strategies
4.3 智能去重模块
支持精确匹配和模糊匹配:
class DeduplicatorEngine:
def __init__(self):
self.removed = 0
def deduplicate(self, df, mode='exact', key_columns=None):
"""智能去重"""
before = len(df)
if mode == 'exact':
df = self._exact_dedup(df, key_columns)
elif mode == 'fuzzy':
df = self._fuzzy_dedup(df, key_columns)
self.removed = before - len(df)
return df
def _exact_dedup(self, df, key_columns=None):
"""精确去重"""
if key_columns:
return df.drop_duplicates(subset=key_columns, keep='first')
return df.drop_duplicates(keep='first')
def _fuzzy_dedup(self, df, key_columns):
"""模糊去重(处理相似但不完全相同的记录)"""
if not key_columns:
return df
# 标准化后去重
df_normalized = df.copy()
for col in key_columns:
if df_normalized[col].dtype == 'object':
df_normalized[col] = df_normalized[col].str.lower().str.strip()
df_normalized[col] = df_normalized[col].str.replace(r'\s+', '', regex=True)
# 标记重复
mask = df_normalized.duplicated(subset=key_columns, keep='first')
return df[~mask]
4.4 格式统一模块
自动识别并统一各种格式:
import re
class FormatStandardizer:
def __init__(self):
self.changes = {}
def standardize(self, df):
"""自动标准化格式"""
for col in df.columns:
col_lower = col.lower()
if any(kw in col_lower for kw in ['日期', 'date', '时间', 'time']):
df[col] = self._standardize_date(df[col])
self.changes[col] = '日期格式统一'
elif any(kw in col_lower for kw in ['手机', '电话', 'phone', 'mobile']):
df[col] = self._standardize_phone(df[col])
self.changes[col] = '手机号格式统一'
elif any(kw in col_lower for kw in ['邮箱', 'email']):
df[col] = self._standardize_email(df[col])
self.changes[col] = '邮箱格式统一'
elif any(kw in col_lower for kw in ['金额', '价格', 'amount', 'price']):
df[col] = self._standardize_amount(df[col])
self.changes[col] = '金额格式统一'
return df
def _standardize_date(self, series):
"""统一日期格式为YYYY-MM-DD"""
def convert(val):
if pd.isna(val):
return val
val = str(val).strip()
formats = ['%Y-%m-%d', '%Y/%m/%d', '%Y年%m月%d日',
'%Y%m%d', '%d/%m/%Y', '%m/%d/%Y',
'%Y.%m.%d', '%d-%m-%Y']
for fmt in formats:
try:
return pd.to_datetime(val, format=fmt).strftime('%Y-%m-%d')
except (ValueError, TypeError):
continue
try:
return pd.to_datetime(val).strftime('%Y-%m-%d')
except:
return val
return series.apply(convert)
def _standardize_phone(self, series):
"""统一手机号格式"""
def convert(val):
if pd.isna(val):
return val
val = str(val).strip()
val = re.sub(r'[^\d]', '', val) # 只保留数字
if val.startswith('86') and len(val) == 13:
val = val[2:]
elif val.startswith('0086') and len(val) == 15:
val = val[4:]
if len(val) == 11 and val.startswith('1'):
return val
return val
return series.apply(convert)
def _standardize_email(self, series):
"""统一邮箱格式"""
def convert(val):
if pd.isna(val):
return val
return str(val).strip().lower()
return series.apply(convert)
def _standardize_amount(self, series):
"""统一金额格式(纯数字)"""
def convert(val):
if
...(truncated)...