AI 可以协助读栏位、写 SQL 与找异常,但 CSV 没有内建 schema,也不保证每列格式一致。正式分析前,至少检查编码与解析、栏位型别、空值、重复列、唯一键、日期时区、单位与异常值;而且要把发现留在 Plan checkpoint 或 SQL artifact,不只留一句聊天提醒。
以下以一份 orders.csv 为例,假设预期栏位有 order_id、customer_id、created_at、amount、currency、status。SQL 采 DuckDB 语法;实际栏名与合法值要换成你的资料契约。
1. 编码、分隔符与解析错误
第一步不是看图,而是确认每列真的被拆成相同栏位。UTF-8/Big5、逗号/分号、引号内逗号与换行都可能让资料错栏。先预览,再检查 parser 推断:
SELECT *
FROM read_csv('orders.csv')
LIMIT 20;
DuckDB CSV 文件提醒 CSV 缺乏 schema 且格式变异多;reader 虽会自动侦测格式与型别,自动结果不符合预期时仍应明确指定 delim、quote、escape 或 header,不要用 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 都能说明处理了什么、没处理什么。