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 都能說明處理了什麼、沒處理什麼。