数据读取与初检函数:进门先看货

做质控分析的第一步是什么?不是建模,不是画图,而是把数据稳稳当当读进来。我记得早些年带新人时,最常见的问题反而不是分析逻辑不对,而是数据都读歪了。

所以这一章,咱们专门聊聊数据读取和初检。说白了,就是把文件从硬盘请进Python,再快速扫一眼——“这数据到底能不能用?”

常见格式数据读取:别让文件格式卡住你

实际项目中,数据源什么妖魔鬼怪都有。我碰过最离谱的一次,客户发来一个 .csv,打开一看里面用逗号分割,但某个字段里又夹了逗号,解析直接崩溃。从那以后,我习惯把文件读取函数统一封装起来,加一道格式校验。

常用的读取函数,我一般会封装成下面这个样子:

import pandas as pd
import json

def load_data(file_path, file_type=None):
    if file_type is None:
        ext = file_path.split('.')[-1].lower()
    else:
        ext = file_type.lower()

    loaders = {
        'csv': lambda: pd.read_csv(file_path),
        'xlsx': lambda: pd.read_excel(file_path),
        'json': lambda: pd.read_json(file_path),
        'parquet': lambda: pd.read_parquet(file_path),
        'feather': lambda: pd.read_feather(file_path),
    }

    if ext not in loaders:
        raise ValueError(f"不支持的格式:{ext}")

    return loaders[ext]()

你问我为啥不直接一行 pd.read_csv?嗯,封装的好处是:一旦哪天你遇上加密 Excel 或 GBK 编码的 CSV,直接在 loaders 里加一句 cleaning 逻辑就行,不用改业务代码。

小提示:我建议多配一个 encoding 参数检测器。很多坑都是编码姿势不对导致的。
def detect_encoding(file_path):
    import chardet
    with open(file_path, 'rb') as f:
        result = chardet.detect(f.read(10000))
    return result['encoding']

数据概览函数封装:一眼看出问题

读取完了,我习惯立刻调一个“快速扫描”函数。为什么?直接跑分析容易翻车,先看概览心里才有底。

def quick_overview(df):
    info = {}
    info['shape'] = df.shape
    info['columns'] = list(df.columns)
    info['dtypes'] = df.dtypes.astype(str).to_dict()
    info['missing_count'] = df.isnull().sum().to_dict()
    info['missing_ratio'] = (df.isnull().sum() / len(df)).to_dict()
    info['duplicate_rows'] = df.duplicated().sum()
    info['memory_usage'] = df.memory_usage(deep=True).sum() / 1024**2

    return info
个人经验:输出里我特别在意 missing_ratio。如果某列缺失率 > 80%,直接考虑扔掉,别浪费时间填充。

我还见过一个同事,每次分析前只瞄一眼 shape 就开始建模,结果跑了一上午,最后发现标签列全是空值。白忙活。

数据类型自动推断:拯救“全是 object”的噩梦

有没有遇到过这种尴尬:读进来的 DataFrame,所有列的类型都是 object?

我之前调试过一个案例,销售日期列是 object,导致按时间聚合时全部报错。原因就是 Excel 里日期格式不统一,pandas 推断失败了。所以我现在都会加一道类型自动纠正。

def auto_infer_types(df):
    for col in df.columns:
        # 尝试转数字
        try:
            df[col] = pd.to_numeric(df[col], errors='ignore')
        except:
            pass
        
        # 尝试转时间
        try:
            df[col] = pd.to_datetime(df[col], errors='ignore')
        except:
            pass
        
        # 如果object列的唯一值少于某阈值,转为分类
        if df[col].dtype == 'object':
            if df[col].nunique() < len(df) * 0.05:
                df[col] = df[col].astype('category')
    return df

你可能会说,pandas 不是有 infer_objects() 吗?没错,但那个太温柔了,很多隐蔽的类型问题它放过去了。我的这个版本,暴力一点,但管用。

注意:自动推断不是万能的。比如“2024/02/30”这种日期,强行转会变成 NaT,但真实世界里这个日期不存在。所以推断之后,一定要跑下一步的基础统计来验证。

基础统计量计算函数:给数据做个体检

开始分析前,我会打一套“组合拳”——连续型变量看分布,离散型变量看频数。

def basic_stats(df):
    results = {}
    
    numeric_cols = df.select_dtypes(include=['int64', 'float64']).columns
    if len(numeric_cols) > 0:
        stats = df[numeric_cols].describe().T
        stats['skew'] = df[numeric_cols].skew()
        stats['kurtosis'] = df[numeric_cols].kurtosis()
        stats['missing'] = df[numeric_cols].isnull().sum()
        results['numeric'] = stats
    
    categorical_cols = df.select_dtypes(include=['object', 'category']).columns
    cat_stats = {}
    for col in categorical_cols:
        cat_stats[col] = {
            'unique_count': df[col].nunique(),
            'top_values': df[col].value_counts().head(5).to_dict(),
            'missing': int(df[col].isnull().sum())
        }
    results['categorical'] = cat_stats
    
    return results

我曾经接手过一个让人头疼的项目,里面有一个特征叫“客户评分”,范围是1-5。我习惯性地算了一下偏度,发现 skew = 0.02,非常完美。但我是怎么发现的呢?——分布全是4分和5分,几乎没有1-3分。偏度骗不了人,但描述性统计里最好配上百分比分布。

来看看我常用的一组决策逻辑:

指标 正常值参考 可能的问题
缺失率 < 5% > 20% 需评估列是否可用
偏度 -1 ~ 1 过偏需检查异常值或取对数
唯一值数量 合理范围内 ID列或错误编码
最大值/最小值 符合业务常识 越界数据(如年龄500)
避坑指南:我曾经直接拿 describe() 的结果去写报告,结果发现“最小值”是负数,但业务上这个字段必须是正数。追查了半天,原来是数据清洗时把缺失值填成了 -999。所以现在我会在 basic_stats 里加一个 min_check 和 max_check 的边界条件。

本章核心知识结构

我把这一套“读取 → 概览 → 推断 → 统计”的流程画了一张图,方便你记忆:

数据读取与初检流程 文件输入 格式识别与封装读取 数据概览扫描 数据类型自动推断 基础统计量计算 输出质控报告 关键点 • 格式校验在先 • 编码检测不能省 • 异常值标记得保留

看完这张图你应该明白了:整个初检流程其实就是一个过滤器,从原始文件一直到质控报告,每一层都在降低数据风险。

最后说一句:这些函数我建议一开项目就写好,不要每次都临时拼凑。我自己就把这套代码放在了一个叫 data_precheck.py 的模块里,项目一来,直接 import,十分钟内就能对数据有个整体判断。省下来的时间,足够你好好喝杯咖啡了。