Skip to content

第 24 章 · 数据速览三件套 ​

这一章要解决什么:建立"拿到任何一张表,先跑这四行"的条件反射。 立下全书第一条建模纪律:标识列和标签列不能当特征。

本章你需要完成的代码:demos/ch24-quick-look.py

24.1 四行标准流程 ​

拿到任何一张表,先跑这四行,别急着做别的:

python
df.shape          # ① 多大规模?
df.head()         # ② 长什么样?
df.info()         # ③ 每列什么类型、有没有缺失?
df.describe()     # ④ 数值范围合不合理?
点开:这四行分别能发现什么问题?

① df.shape → (13967, 9)

  • 行数对不对?(预期 1.4 万,拿到 4 千 → 上游漏了数据)
  • 列数对不对?(预期 9 列,拿到 12 列 → 上游加了字段)

② df.head()

这是信息量最大的一步,因为它让你看到真实的列名和值:

text
cell_id batch  cycle  timestamp  capacity_ah    soh  resistance_mohm  temp_rise_c  coulombic_eff
   C001     A      1 2023-01-01       2.5092 0.9999            40.33         6.00          0.998
   C001     A      2 2023-01-02       2.4800 0.9998            39.47         6.01          0.998
   C001     A      3 2023-01-03       2.4862 0.9997            39.85         6.01          0.998

注意这里发现的两件事:

  • timestamp 是 2023-01-01 这样的字符串,不是日期类型;
  • soh 在前 3 行几乎等于 1 —— 说明电池刚上工还没衰减(合理)。

③ df.info()

text
 #   Column           Non-Null Count  Dtype
 0   cell_id          13967 non-null  str
 1   batch            13967 non-null  str
 2   cycle            13967 non-null  int64
 3   timestamp        13967 non-null  str
 4   capacity_ah      13967 non-null  float64
...
memory usage: 982.2 KB

一眼看出:所有列都是 13967 non-null(没有缺失)、 哪些是字符串哪些是数值、占多少内存。

④ df.describe()

text
            cycle  capacity_ah         soh  resistance_mohm  temp_rise_c  coulombic_eff
count  13967.0000   13967.0000  13967.0000       13967.0000   13967.0000     13967.0000
mean     601.1927       2.2679      0.9071          54.8383      10.6421         0.9960
std      365.6767       0.1486      0.0593          11.6132       2.9772         0.0012
min        1.0000       1.9781      0.8000          38.8100       6.0000         0.9940
25%      291.5000       2.1405      0.8560          44.5200       7.9850         0.9950
50%      582.0000       2.2743      0.9099          52.8000      10.5000         0.9960
75%      876.0000       2.4001      0.9601          63.4200      13.2000         0.9970
max     1423.0000       2.5216      1.0000          87.5700      17.2000         0.9980

这是"物理合理性检查"最快的方式:

列minmax合理吗
cycle11423✅ 循环次数从 1 开始
capacity_ah1.97812.5216✅ 额定 2.5 Ah,在合理范围
soh0.80001.0000✅ 正好落在 [0.80, 1.00](数据生成规则)
resistance_mohm38.8187.57✅ 几十 mΩ 是正常范围
temp_rise_c6.0017.20✅ 温升十几度合理
coulombic_eff0.99400.9980✅ 库仑效率接近 1

如果某一列的 max 是 562(就像 dirty.csv 里的内阻),你立刻就会发现异常。

count 还有一个用途:如果 count 与行数不等,说明有缺失。

text
count  13967.0000     ← 等于行数 → 无缺失
count  13688.0000     ← 少了 279 → 有缺失,去 isna().sum() 看哪里缺

24.2 info():一个坑要提前说 ​

python
df.info()          # 直接打印到终端

x = df.info()      # ❌ 拿到的是 None!
print(x)           # None

为什么:df.info() 是打印到 stdout,不是返回值。 它没有 return 语句,所以返回 None。

这是 Pandas 里少数几个"打印型"方法。其余大多数方法都返回新对象:

python
x = df.head()          # ✅ 返回 DataFrame
y = df.describe()      # ✅ 返回 DataFrame
z = df.info()          # ❌ 返回 None(它是打印型)

记住这一个就够了。 其他的如果拿到 None,先想想"这个方法是不是打印型的"。

24.3 摸清每一个字段 ​

describe 只看数值列。要摸清所有字段,还需要三个工具:

python
df.isna().sum()          # 每列缺几个
df.nunique()             # 每列有几个不同的值
df["batch"].value_counts()   # 某个类别列的取值分布

真实输出:

text
唯一值个数 df.nunique():
cell_id              12
batch                 4
cycle              1423
timestamp          1423
capacity_ah        4839
soh                2001
resistance_mohm    3986
temp_rise_c        1075
coulombic_eff        41

batch 的取值分布:
batch
A    3331
B    4157
C    3304
D    3175

从 nunique 能读出很多信息:

列nunique推断
cell_id1212 支电芯 → 这是分组键,不是特征
batch44 个批次 → 这是类别列,要编码
cycle1423连续量(循环次数)
timestamp1423和 cycle 的 nunique 一样 → 两者一一对应(1 圈 = 1 天)
coulombic_eff41只有 41 个不同值 → 精度受限(数据生成时保留 4 位小数)

value_counts() 的用途:看类别列的平衡性。

python
print(df["batch"].value_counts())
# A    3331
# B    4157
# C    3304
# D    3175

四个批次样本数接近但有差异 —— 因为每支电芯的寿命不同(859~1423 圈), 所以批次的总行数也不同。这是合理的,不是数据错误。

如果出现极端不平衡(比如 A 有 12000 行、B 只有 50 行), 就要警惕:可能是数据采集有偏,或者某个批次的定义有问题。

24.4 把列分成三类:本章的重点 ​

python
num_cols = df.select_dtypes(include="number").columns.tolist()
str_cols = df.select_dtypes(exclude="number").columns.tolist()

print(num_cols)
# ['cycle', 'capacity_ah', 'soh', 'resistance_mohm', 'temp_rise_c', 'coulombic_eff']
print(str_cols)
# ['cell_id', 'batch', 'timestamp']

select_dtypes 是"按类型筛列"的工具,在数据清洗里极其常用 (比如第 30 章要"只对数值列插值")。

等价写法(用列表推导,第 11 章学的):

python
str_cols2 = [c for c in df.columns if not pd.api.types.is_numeric_dtype(df[c])]
assert str_cols == str_cols2

两种写法都对。 select_dtypes 更短,列表推导更灵活(能做更复杂的判断)。

24.5 ★ 第一课建模纪律:这三列该不该进模型 ​

这是本章最重要的内容,也是全书反复出现的一条纪律。

拿到一张表,对每一个非数值列都要问一句:

"这一列的唯一作用,是不是只用来『认出』某条记录?"

如果是 → 它不能作为特征。

点开:逐列审问 cell_id / batch / timestamp / soh

cell_id → 不进特征 ​

理由:

  • 它只有 12 个取值,本质是"编号",和数据规律无关;
  • 它唯一的作用是"认出这是哪支电芯";
  • 模型学到"编号 5 的电芯更好"是纯粹的噪声(换一批数据编号就变了)。

但它非常有用 —— 作为分组键:

python
df.groupby("cell_id")["soh"].mean()      # 每支电芯的平均 SOH(第 28 章)
df.groupby("cell_id")["soh"].transform("mean")   # 组内均值贴回原表

关键认知:"不能当特征"不等于"没用"。 标识列在聚合、连接、分组里是第一等公民。

batch → 可以进,但要先编码 ​

batch 是类别列(A/B/C/D)。模型只认数字,所以要转换成数字:

python
# 方式一:映射成整数(有序时用,比如"小/中/大")
df["batch_code"] = df["batch"].map({"A": 0, "B": 1, "C": 2, "D": 3})

# 方式二:one-hot 编码(无序时用,也是默认推荐)
pd.get_dummies(df, columns=["batch"], prefix="batch")
# 会生成 batch_A / batch_B / batch_C / batch_D 四个 0/1 列

⚠ 一个坑:cell_id 和 batch 有从属关系(每支电芯属于一个批次)。 如果两个都放进模型,会造成信息重复。

本书的做法:处理电池数据时不把 batch 当特征, 而是用 groupby("batch") 做对比分析(第 38 章的箱线图)。

timestamp → 不进特征,但可以派生 ​

直接喂给模型没意义:2023-01-01 对模型来说只是一个编号。 而"2023 比 2022 大"这个信息是伪信息(年份的大小和数据规律无关)。

但可以派生出真正有信息量的特征:

python
df["days_since_start"] = (df["timestamp"] - df["timestamp"].min()).dt.days
# 距首次使用的天数 —— 单调递增,有趋势
df["month"] = df["timestamp"].dt.month
# 月份 —— 有周期性(如果和季节相关)
df["dayofweek"] = df["timestamp"].dt.dayofweek
# 周几 —— 有周期性(如果和生产排班相关)

这就是"时间特征工程"(第 29 章详讲)。

判断标准:派生的特征要有物理/业务含义。 "距首次使用 300 天"意味着"老化程度",这是有意义的; "年份 = 2023"没有意义。

soh → 不进特征!这是标签 ​

这是最危险的一个。

soh 是我们要预测的目标(标签)。把它当特征,等于把答案抄进试卷。

后果:

python
# 错误做法
X = df[["cycle", "capacity_ah", "resistance_mohm", "temp_rise_c", "soh"]]
y = df["soh"]
model.fit(X, y)
print(model.score(X, y))       # 1.0 !!完美

模型会学到"如果 soh 等于 0.92,那么答案就是 0.92" —— 所以准确率 100%。 上线后立刻崩,因为线上没有 soh 这一列(那正是要预测的东西)。

这个错误有个专门的名字:特征泄漏(data leakage)。 第 37 章会用相关系数 0.998 再坐实一次。

判断"是不是标签/标识"的两条实用启发式 ​

启发式一:看 nunique。

python
n = len(df)
for c in df.columns:
    uniq = df[c].nunique()
    if uniq == n:
        tag = "每行都不同 → 典型的标识列"
    elif uniq < 20:
        tag = f"取值很少({uniq} 个)→ 可能是类别列"
    else:
        tag = "连续量或时间"

真实输出:

text
cell_id          nunique=     12  取值很少(12 个)→ 可能是类别列
batch            nunique=      4  取值很少(4 个)→ 可能是类别列
cycle            nunique=  1,423  连续量或时间
timestamp        nunique=  1,423  连续量或时间
capacity_ah      nunique=  4,839  连续量或时间
soh              nunique=  2,001  连续量或时间
resistance_mohm  nunique=  3,986  连续量或时间
temp_rise_c      nunique=  1,075  连续量或时间
coulombic_eff    nunique=     41  连续量或时间

⚠ 但这个启发式会误判:cell_id 只有 12 个唯一值,看起来像"类别列", 但它依然是标识列。

为什么:因为 12 是"电芯的数量",不是"这一列的价值" —— 如果这批数据有 12 万支电芯,cell_id 的 nunique 就是 120000, 但它的性质没变。

所以 nunique 只是线索,最终要靠业务判断。

启发式二:问三个问题。

① 这一列在"预测发生的那一刻"能拿到吗?
   拿不到 → 它可能是标签,或者是未来信息 → 不能用

② 这一列的取值会随数据集变化而失去意义吗?
   会(编号、ID)→ 标识列 → 不能当特征

③ 这一列是不是从标签算出来的?
   是 → 特征泄漏 → 必须剔除

第 37 章会看到这三条启发式的完整实战。

24.6 最后:一句话速览 ​

速览的目的是"能在 30 秒内向别人介绍这张表":

python
per = df.groupby("cell_id").agg(n=("cycle", "size"),
                                soh_end=("soh", "last")).sort_values("n")
print(per)

真实输出(节选):

text
         n  soh_end
cell_id
C003   859      0.8
C001   862      0.8
C012   879      0.8
...
C007  1415      0.8

一句话速览:

12 支电芯、4 个批次,每支从第 1 圈跑到 SOH 降到 0.80 为止, 寿命 859~1423 圈,共 13,967 条记录、9 个字段; 无缺失;数值范围全部落在物理合理区间。

这就是"数据速览"的产出 —— 一段能被别人听懂的话。 它比"我读了一个 CSV"有价值得多。

24.7 完整示例与输出 ​

bash
cd code
python demos/ch24-quick-look.py

真实输出(节选):

text
=== ② info():一眼看全列名/类型/非空数/内存 ===
 #   Column           Non-Null Count  Dtype
 0   cell_id          13967 non-null  str
 3   timestamp        13967 non-null  str
 4   capacity_ah      13967 non-null  float64
dtypes: float64(5), int64(1), str(3)
memory usage: 982.2 KB

=== ③ describe():数值列的统计摘要 ===
            cycle  capacity_ah         soh  resistance_mohm
count  13967.0000   13967.0000  13967.0000       13967.0000
mean     601.1927       2.2679      0.9071          54.8383
min        1.0000       1.9781      0.8000          38.8100
max     1423.0000       2.5216      1.0000          87.5700

=== ⑤ 把列分成三类:这是本章真正的重点 ===
  数值列(6):['cycle', 'capacity_ah', 'soh', 'resistance_mohm', 'temp_rise_c', 'coulombic_eff']
  非数值列(3):['cell_id', 'batch', 'timestamp']

=== ⑦ 用 nunique 自动识别「疑似标识列」 ===
  cell_id          nunique=     12  取值很少(12 个)→ 可能是类别列
  ⚠ 这只是启发式。cell_id 只有 12 个唯一值但依然是标识列 —— 最终要靠业务判断。

本章练习 ​

练习 1:跑一遍速览流程

对 cycle_raw.csv 依次跑:shape、head(3)、info()、describe()、dtypes、isna().sum()、nunique(),然后说出:

  1. 有多少行多少列?
  2. 有没有缺失?
  3. 哪些列是数值、哪些不是?
  4. 有没有哪一列的取值范围看起来"不对劲"?
参考答案
python
from pathlib import Path
import pandas as pd

pd.set_option("display.width", 160)
CODE_DIR = Path(__file__).resolve().parent.parent.parent
df = pd.read_csv(CODE_DIR / "data" / "battery" / "cycle_raw.csv")

# ① 规模
print(df.shape)              # (13967, 9)
# → 13,967 行 × 9 列

# ② 缺失
print(df.isna().sum().sum()) # 0
# → 完全没缺失(这是干净数据)

# ③ 类型
print(df.dtypes)
num = df.select_dtypes("number").columns.tolist()
obj = df.select_dtypes(exclude="number").columns.tolist()
print(f"数值列 {len(num)}: {num}")
print(f"非数值列 {len(obj)}: {obj}")
# 数值列: cycle, capacity_ah, soh, resistance_mohm, temp_rise_c, coulombic_eff
# 非数值列: cell_id, batch, timestamp

# ④ 范围检查
print(df.describe().loc[["min", "max"]].T)

四列的范围检查结论:

列minmax判断
cycle11423✅ 从 1 开始,合理
capacity_ah1.97812.5216✅ 额定 2.5,实测在附近
soh0.80001.0000✅ 正好 [0.80, 1.00](生成规则)
resistance_mohm38.8187.57✅ 几十 mΩ 正常
temp_rise_c6.0017.20✅ 温升十几度合理
coulombic_eff0.99400.9980✅ 库仑效率接近 1

没有"不对劲"的列 —— 因为这是干净数据。

对比一下 dirty.csv:

python
dirty = pd.read_csv(CODE_DIR / "data" / "battery" / "dirty.csv")
print(dirty.describe().loc[["min", "max"]].T)
# capacity_ah      min = -0.5      ❌ 容量为负!
# resistance_mohm  max = 562.61    ❌ 内阻超高!

这就是 describe() 最大的价值: 两行代码就能发现"物理上不可能"的值。

注意 info() 是打印型:

python
x = df.info()       # 会打印一堆东西
print(x)            # None  ← 因为它没有 return

完整练习:exercises/ex24-01-quick-look

练习 2:逐列审问"该不该进模型"

对下面四列写出判断和理由:

  1. cell_id(电芯编号,12 个取值)
  2. batch(批次 A/B/C/D)
  3. timestamp(日期字符串)
  4. soh(健康度)

如果目标是"预测 SOH",它们分别该怎么用?

参考答案
列进模型?理由该怎么用
cell_id❌ 不进标识列。取值只是编号,和规律无关;换批数据编号就变分组键 —— groupby("cell_id")
batch⚠️ 可以进,但要编码类别列。模型只认数字pd.get_dummies();但本书用它做对比分析(第 38 章)
timestamp❌ 不进特征日期字符串对模型无意义("2023 比 2022 大"是伪信息)派生成 days_since_start / month / dayofweek(第 29 章)
soh❌ 绝对不能进它是标签!放进去 = 抄答案预测目标 y = df["soh"]

★ 关于 soh:这是最危险的一个

python
# 错误做法
FEATURES = ["cycle", "capacity_ah", "resistance_mohm", "temp_rise_c", "soh"]  # ← 混进了 soh
X = df[FEATURES]
y = df["soh"]

from sklearn.linear_model import LinearRegression
model = LinearRegression().fit(X, y)
print(model.score(X, y))     # 1.0(或接近 1.0)—— 完美!

看着很美好,实际上模型什么都没学到。它只是发现了 "当 soh 列的值是 0.92 时,答案是 0.92"。

上线时会立刻崩:因为线上根本拿不到 soh 这一列 —— 那正是要预测的东西。

这个错误的名字:特征泄漏(data leakage)。

第 37 章会用相关系数 0.998 再坐实一次,并给出识别它的三条启发式。

★ 关于 cell_id:一个容易忽略的点

python
# 不能当特征
X = df[["cell_id", "cycle"]]        # ❌ cell_id 是字符串,而且没有意义

# 但作为分组键非常有用
df.groupby("cell_id")["soh"].mean()                          # 每支电芯的平均 SOH
df.groupby("cell_id")["resistance_mohm"].rank(pct=True)       # 组内排名

"不能当特征" ≠ "没用"。 标识列在聚合、连接、分组里是第一等公民。

★ 关于 batch:为什么本书不用它当特征

batch 和 cell_id 有从属关系(每支电芯属于一个批次)。 如果两个都放进去,会造成信息重复(第 37 章的"多重共线性")。

而且对本书的数据来说,四个批次来自同一个生成过程, 批次间没有真实差异(第 38 章的箱线图会证实这一点)。

所以本书的用法是:

python
# 用 batch 做"对比分析",而不是当特征
df.groupby("batch")["soh"].describe()      # 看四批的分布是否一致

真实项目里的经验:如果 batch 真的有差异(比如不同产线), 就应该编码后放进模型 —— 那正是模型该学的东西。 判断标准是"业务上它是否携带信息",而不是机械地排除。

练习 3:nunique 能告诉你什么
python
df.nunique()

真实输出:

text
cell_id              12
batch                 4
cycle              1423
timestamp          1423
capacity_ah        4839
soh                2001
resistance_mohm    3986
temp_rise_c        1075
coulombic_eff        41

要求:从这些数字里能推断出什么?至少说出三条。

参考答案

推断一:cycle 和 timestamp 的 nunique 完全一样(都是 1423)

说明两者一一对应 —— 每个 cycle 值对应唯一的 timestamp。 结合"1 次循环 = 1 天"的设计,可以确认:

python
print((df.groupby("cycle")["timestamp"].nunique() == 1).all())    # True

这个检查很有用:如果哪天两者不一致,说明数据的时间字段有问题。

推断二:cell_id 有 12 个取值 → 12 支电芯

结合行数 13,967:

python
print(f"平均每支 {len(df) / df['cell_id'].nunique():.0f} 圈")
# 平均每支 1164 圈

和 cycle 的 max(1423)对比:平均 1164 < 最大 1423, 说明每支电芯的寿命不同(859~1423 圈)。

推断三:coulombic_eff 只有 41 个不同值

这说明这一列的精度有限。看公式:

python
eff = np.clip(0.998 - 0.004 * c, 0.985, 0.999)     # 然后 round(4)

只有 4 位小数,而且取值范围窄(0.985~0.999),所以不同值少。

这带来一个实际问题:

python
print(df["coulombic_eff"].value_counts().head())
# 0.9980    4933
# 0.9960    3727
# 0.9970    2447
# ...

大量重复值 —— 如果用它当特征,区分度可能有限。 (但它的相关系数是 0.98,信息量其实是够的。)

推断四:capacity_ah 有 4839 个不同值

对比 soh 的 2001 个 —— capacity_ah 的取值更分散。

为什么:soh 是按公式算出来的(只有 2001 个不同值), 而 capacity_ah 是 soh × 2.5 × (1 + 噪声) —— 噪声让它更离散。

这印证了第 27 章要讲的一点:capacity_ah 和 soh 不是完全等价的 (前者带测量噪声),所以 soh 是"真值",capacity_ah 是"实测"。

推断五(方法论):nunique 是发现"数据异常"的快捷方式

python
# 如果某列 nunique == 1,说明它是常量列 —— 对模型毫无用处
constant_cols = [c for c in df.columns if df[c].nunique() == 1]
print(constant_cols)        # []

常量列要直接删掉:它不提供任何信息,只会增加维度。

如果某列 nunique == len(df),说明每行都不同 —— 疑似标识列(见 24.5)。

一个综合的字段审查脚本(可以直接抄走):

python
def audit_columns(df):
    """逐列输出:类型、缺失率、唯一值数、样例值。"""
    n = len(df)
    for c in df.columns:
        s = df[c]
        n_uniq = s.nunique()
        miss = s.isna().mean()
        sample = s.dropna().iloc[0] if s.notna().any() else None
        # 猜测用途
        if n_uniq == n:
            guess = "疑似标识列"
        elif n_uniq == 1:
            guess = "常量列(建议删除)"
        elif n_uniq <= 20:
            guess = "类别列(需编码)"
        else:
            guess = "连续量/时间"
        print(f"{c:<18} {str(s.dtype):<10} 缺失{miss:>6.2%} 唯一{n_uniq:>7,d}  {guess}")

audit_columns(df)

面向 AI 开发:describe() 是模型上线前的最后一道检查 ​

在真实项目里,describe() 有一个超出"数据探索"的用途:监控线上数据漂移。

什么是数据漂移(data drift):模型训练时看到的数据分布, 和上线后遇到的数据分布不一样。

一个真实场景:

python
# 训练时记录各列的范围
train_stats = X_train.describe().loc[["mean", "std", "min", "max"]]
train_stats.to_csv("train_stats.csv")

# 上线后每天对比
prod_stats = X_today.describe().loc[["mean", "std", "min", "max"]]
diff = (prod_stats - train_stats).abs() / train_stats.abs()
if (diff["mean"] > 0.2).any():
    print("⚠️ 有特征均值漂移超过 20%,检查上游数据")

为什么这件事重要:模型的性能下降,90% 的原因不是模型退化,而是输入数据变了:

漂移类型例子影响
均值漂移换了新的传感器(读数系统性偏移)模型预测整体偏
方差漂移换了更稳定的工艺模型在"从未见过"的区间预测
分布形状漂移出现新工况模型对尾部预测不可靠
缺失模式漂移某个传感器开始掉线填充策略失效

describe() 是最低成本的漂移检测工具。

更进一步:PSI(Population Stability Index)

describe() 只能看均值和方差。要看整个分布,用分箱对比:

python
def psi(expected, actual, bins=10):
    """分布稳定性指标。> 0.25 通常认为分布显著变化。"""
    edges = np.percentile(expected, np.linspace(0, 100, bins + 1))
    edges[0], edges[-1] = -np.inf, np.inf
    e = np.histogram(expected, bins=edges)[0] / len(expected)
    a = np.histogram(actual, bins=edges)[0] / len(actual)
    e, a = np.clip(e, 1e-6, None), np.clip(a, 1e-6, None)
    return float(((a - e) * np.log(a / e)).sum())

经验阈值:

PSI含义
< 0.1分布稳定
0.1 ~ 0.25有变化,需要关注
> 0.25显著变化,模型需要重训

为什么 AI 从业者要懂这些: 一个模型上线后,它不是"完成了",而是"开始被监控"。 而监控的第一层就是"输入数据还和训练时一样吗"。

本章的四个速览命令就是监控脚本的第一行代码。

最后一个实用建议:把本章的速览包装成一个函数, 放在每个数据处理脚本的开头调用。

python
def quick_audit(df, name=""):
    """数据速览:打印规模、类型、缺失、范围。"""
    print(f"\n{'='*60}\n{name or 'DataFrame'} 速览\n{'='*60}")
    print(f"形状: {df.shape}")
    print(f"\n类型与缺失:")
    info = pd.DataFrame({
        "dtype": df.dtypes.astype(str),
        "缺失数": df.isna().sum(),
        "缺失率": df.isna().mean().round(4),
        "唯一值": df.nunique(),
    })
    print(info.to_string())
    num = df.select_dtypes("number")
    if not num.empty:
        print(f"\n数值列范围:")
        print(num.describe().loc[["min", "mean", "max"]].T.round(4).to_string())
    return df

# 用法
df = quick_audit(pd.read_csv(path), "原始数据")

这个函数的价值:它把"四行标准流程"固化成一个动作, 保证你不会因为赶时间而跳过检查 —— 而跳过检查,是数据项目翻车的头号原因。


下一章:第 25 章 · 选列与选行:loc / iloc