Lantide Data
返回博客
数据分析实践

用 AI 分析 CSV 前,先做这 8 个资料品质检查

AI 能快速探索 CSV,但不会替你定义正确资料。本文提供编码、型别、空值、重复、键、时区、单位与异常值的 SQL 检查表,并示范如何留下可重跑 checkpoint。

AI 可以协助读栏位、写 SQL 与找异常,但 CSV 没有内建 schema,也不保证每列格式一致。正式分析前,至少检查编码与解析、栏位型别、空值、重复列、唯一键、日期时区、单位与异常值;而且要把发现留在 Plan checkpoint 或 SQL artifact,不只留一句聊天提醒。

以下以一份 orders.csv 为例,假设预期栏位有 order_idcustomer_idcreated_atamountcurrencystatus。SQL 采 DuckDB 语法;实际栏名与合法值要换成你的资料契约。

1. 编码、分隔符与解析错误

第一步不是看图,而是确认每列真的被拆成相同栏位。UTF-8/Big5、逗号/分号、引号内逗号与换行都可能让资料错栏。先预览,再检查 parser 推断:

SELECT *
FROM read_csv('orders.csv')
LIMIT 20;

DuckDB CSV 文件提醒 CSV 缺乏 schema 且格式变异多;reader 虽会自动侦测格式与型别,自动结果不符合预期时仍应明确指定 delimquoteescapeheader,不要用 ignore_errors = true 静默略过后就开始算。核心 CSV reader 直接支援 UTF-8、UTF-16 与 Latin-1;Big5 等其他编码需确认环境已安装并载入 encodings extension,否则先转成 UTF-8。DuckDB Encodings Extension

2. 栏位型别:数字有没有被当文字?

先用 DuckDB DESCRIBE 看推断后的 schema:

DESCRIBE SELECT * FROM read_csv('orders.csv');

金额若混入千分位、币别字串或 N/A,可能整栏变成 VARCHAR;日期若格式混杂,也可能无法排序。正式计算前用 TRY_CAST 量化转换失败:

SELECT
  count(*) AS rows,
  count(*) FILTER (
    WHERE amount IS NOT NULL
      AND TRY_CAST(amount AS DECIMAL(18, 2)) IS NULL
  ) AS invalid_amount_rows
FROM read_csv('orders.csv', all_varchar = true);

3. 空值:NULL、空字串与占位符要分开

CSV 里的空白可能被解读成 NULL,也可能是 ''N/A-unknown 又是另一种。不要只算 IS NULL

SELECT
  count(*) AS rows,
  count(*) FILTER (WHERE order_id IS NULL OR trim(order_id) = '') AS missing_order_id,
  count(*) FILTER (WHERE customer_id IS NULL OR trim(customer_id) = '') AS missing_customer,
  count(*) FILTER (WHERE amount IS NULL) AS missing_amount
FROM read_csv('orders.csv', all_varchar = true);

哪些空值可以保留、补值或排除,是业务决定,不是 Agent 自动猜测。

4. 重复列:档案 append 了两次吗?

完全相同的列常来自重复汇出或合并。先比较总列数与 distinct row:

WITH src AS (
  SELECT * FROM read_csv('orders.csv')
)
SELECT
  (SELECT count(*) FROM src) AS rows,
  (SELECT count(*) FROM (SELECT DISTINCT * FROM src)) AS distinct_rows;

若 SQL 引擎或栏位组合不适合整列 distinct,改用明确的业务栏位 group by。删除 duplicate 前要保留来源与判断规则,因为两笔看似相同的事件也可能合法。

5. 唯一键:order_id 真的一列一单吗?

唯一键决定资料 grain,也影响后续 JOIN 是否 fan-out:

SELECT order_id, count(*) AS n
FROM read_csv('orders.csv')
GROUP BY order_id
HAVING count(*) > 1
ORDER BY n DESC
LIMIT 50;

如果一个订单可有多个品项,order_id 不唯一可能完全合理;此时真正 grain 可能是 order_id + line_id。重点不是消灭重复,而是写下正确粒度。

6. 日期与时区:跨日不是格式问题而已

先量化可解析率与范围:

SELECT
  min(TRY_CAST(created_at AS TIMESTAMPTZ)) AS min_ts,
  max(TRY_CAST(created_at AS TIMESTAMPTZ)) AS max_ts,
  count(*) FILTER (
    WHERE created_at IS NOT NULL
      AND TRY_CAST(created_at AS TIMESTAMPTZ) IS NULL
  ) AS invalid_ts
FROM read_csv('orders.csv', all_varchar = true);

还要确认来源是 UTC、台北时间,或没有 offset 的 local time。月末、日界线与 daylight saving time 会改变 cohort;在 Plan 写清楚报表时区,再转换与截日。

7. 单位与币别:相同栏位不等于相同尺度

amount = 100 可能是元、分、美元或新台币。先看所有币别与量级:

SELECT currency, count(*) AS rows,
       min(TRY_CAST(amount AS DOUBLE)) AS min_amount,
       max(TRY_CAST(amount AS DOUBLE)) AS max_amount
FROM read_csv('orders.csv', all_varchar = true)
GROUP BY currency
ORDER BY rows DESC;

若要换汇,汇率日期与来源也是分析口径;不要让 AI 自行选一个「合理」汇率。

8. 异常值与合法范围:先标记,不要直接删

负金额可能是退款,也可能是错误;极大值可能是企业订单。先用分位数与业务规则找出候选:

WITH typed AS (
  SELECT TRY_CAST(amount AS DOUBLE) AS amount
  FROM read_csv('orders.csv', all_varchar = true)
)
SELECT
  min(amount) AS min_amount,
  quantile_cont(amount, 0.5) AS median,
  quantile_cont(amount, 0.99) AS p99,
  max(amount) AS max_amount
FROM typed;

异常值处理要保留原值、标记原因与排除前后影响。统计上的 outlier 不必然是业务错误。

把八项检查变成 Plan checkpoint

不要只把结果贴在聊天里。正式分析可以在 Plan 写成:

Checkpoint A:确认 parser、栏位型别与日期解析失败率
Checkpoint B:确认 grain、唯一键与重复列处理
Checkpoint C:确认报表时区、币别与单位
Checkpoint D:列出空值与异常值规则,报告排除前后影响

在 Lantide Data 中,Agent 可先探索本机 CSV/Excel/Parquet 与 schema,再把检查写进 Plan;使用者审阅后才 Execute。查询逻辑留在持久 SQL 分页,结果可快取供后续步骤引用,Report 则应揭露限制。这让「资料有问题」从聊天提醒变成可重跑、可验收的 evidence。操作可参考 SQL-first 工作流Plan → Execute → Report

Lantide 不会自动把脏资料变正确,也不会替业务 owner 决定空值、退款与汇率口径;它提供的是本机 DuckDB 查询层与可审阅 artifacts。Local-first 也不代表使用外部模型时资料必然不会进入模型 context,仍需检查 AI profile 与连线设定。

结语

先把八项检查跑在一小段资料上,确认解析、grain 与时间/单位,再让 AI 进入正式分析。最值得自动化的不是「跳过资料品质」,而是把同一套品质 SQL 保存、重跑,并让每次 Report 都能说明处理了什么、没处理什么。

参考资料