The project question sounded simple: count historical menus by city, decade, and meal type. The raw NYPL menu table made every part of that question unstable. Place values mixed cities, addresses, hotels, ships, and routes; dates contained missing and impossible years; event labels used many languages and spellings.
The goal was not to make every field look tidy. It was to make one analysis question answerable while preserving uncertainty and source evidence.
Define the output before the cleaning rules
The 17,545 input rows were preserved. Raw columns remained untouched; fourteen derived fields recorded normalized values and provenance, including:
canonical_city, city_source, city_rule, is_transit
menu_year, menu_decade, date_status, date_rule
canonical_event, event_source, event_rule
This made the cleaned dataset (D’) an extension of (D), not a replacement pretending the original ambiguity never existed.
The pipeline had two levels
The outer workflow profiled, cleaned, validated, audited inference, and packaged evidence. The inner workflow processed each row through nine conceptual stages:
- normalize text;
- classify ship, train, and en-route records;
- extract a city candidate from
place; - tally location evidence and veto ambiguous venue names;
- apply a curated gazetteer and learned crosswalk;
- resolve the final city by fixed precedence;
- parse and repair dates;
- map event text into meal types;
- emit provenance and a change log.
One step required dataset-level evidence: a hotel name seen in several cities could not safely imply one city. Row-by-row code alone could not discover that ambiguity.
In plain terms: a value can look obvious in one row and become unsafe after seeing the rest of the table.
Canonicalization changed the analytical grain
The pipeline reduced 3,708 distinct raw place strings to 233 canonical cities and 1,769 event strings to eight meal types. DINNER alone absorbed 736 raw variants.
Before cleaning, the analysis returned 7,559 menus across 5,049 groups. Ships dominated the largest groups, and synonymous meal labels fragmented counts. After cleaning, it returned 6,268 attributable menus across 757 groups.
The lower row count was intentional:
7,559 raw answer rows
- 2,858 transit menus
- 1,259 non-city or unresolved place values
+ 2,825 cities recovered from location
+ 1 date repaired into range
= 6,268 cleaned answer rows
Cleaning did not maximize coverage. It changed the population from “rows with something in place” to “menus attributable to a city.”
Inference was measured, not declared correct
When place was unusable, location could sometimes recover a city through a curated venue gazetteer or a learned majority crosswalk. The learned mapping required support of at least three and confidence of at least 0.75; generic venue names that mapped to several cities were vetoed.
A held-out check used rows where both signals were available:
| Inference path | Rows checked | Agreement |
|---|---|---|
| Curated gazetteer | 381 | 99.7% |
| Learned crosswalk, leave-one-out | 455 | 93.6% |
| Combined | 836 | 96.4% |
The caveat matters: these test rows had a parseable place and may be easier than rows requiring inference. The measured agreement is evidence, not a guarantee for the target population.
Constraints protected the interpretation
Eight denial-style constraints checked transit values used as cities, impossible dates, missing IDs, meal vocabulary, non-city place values, and provenance for derived values. All post-cleaning counts were zero.
Missing dates were reported separately from invalid dates. There were 586 missing dates but only six present dates outside the accepted range before cleaning. Combining them would have produced a more dramatic but less honest improvement.
What the project taught me
- Preserve raw fields and put interpretations in derived columns.
- Profile globally before trusting a local pattern.
- Treat controlled vocabularies as explicit analytical decisions.
- Reconcile every row entering or leaving an answer set.
- Measure inference on held-out evidence and state sample bias.
- Make rules, thresholds, tests, and provenance part of the deliverable.
The finished pipeline used standard-library Python and SQLite, with 44 unit tests and machine-readable audit reports. Tool choice mattered less than making every cleaning decision inspectable and rerunnable.
Review card
- Start from the analytical question and its required grain.
- Preserve raw data; add canonical values and provenance.
- Dataset-level ambiguity can defeat row-local rules.
- Better quality may legitimately reduce coverage.
- Reconcile before-and-after populations rather than comparing only totals.
- An accuracy percentage needs a sampling caveat.
Project 問題看起來很簡單:依 city、decade、meal type 計算 historical menu 數量。Raw NYPL menu table 卻讓三個維度都不穩定:place 混合 city、address、hotel、ship 與 route;date 有 missing 和 impossible year;event label 使用多種語言與拼法。
目標不是讓每個 field 看起來整齊,而是讓一個分析問題可回答,同時保留 uncertainty 與 source evidence。
先定義 output,再寫 cleaning rule
17,545 筆 input row 全部保留。Raw column 不覆寫,另加 14 個 derived field 保存 normalized value 與 provenance,包括:
canonical_city, city_source, city_rule, is_transit
menu_year, menu_decade, date_status, date_rule
canonical_event, event_source, event_rule
因此 cleaned dataset (D’) 是 (D) 的 extension,不是假裝原始 ambiguity 從未存在的 replacement。
Pipeline 有兩個層次
Outer workflow 負責 profile、clean、validate、audit inference 與 package evidence。Inner workflow 讓每筆 row 經過九個 conceptual stage:
- normalize text;
- classify ship、train、en-route record;
- 從
placeextract city candidate; - tally location evidence,veto ambiguous venue name;
- 套 curated gazetteer 與 learned crosswalk;
- 依固定 precedence 決定 final city;
- parse/repair date;
- 將 event text 映射為 meal type;
- emit provenance 與 change log。
其中一個 step 必須看 dataset-level evidence:同一 hotel name 若出現在不同 city,就不能安全推成單一 city。只看單 row 的 code 發現不了這種 ambiguity。
白話來說: 某個值單獨看很明顯,看到整張 table 後可能就不再安全。
Canonicalization 改變 analytical grain
Pipeline 將 3,708 個 distinct raw place string 收斂成 233 個 canonical city,1,769 個 event string 收斂成八種 meal type;光 DINNER 就吸收 736 個 raw variant。
Cleaning 前,analysis 回傳 7,559 份 menu、5,049 個 group;最大 group 被 ship 占據,同義 meal label 又把 count 切碎。Cleaning 後,結果是 6,268 份可歸屬 menu、757 個 group。
較少的 row 是刻意結果:
7,559 raw answer rows
- 2,858 transit menus
- 1,259 non-city or unresolved place values
+ 2,825 cities recovered from location
+ 1 date repaired into range
= 6,268 cleaned answer rows
Cleaning 沒有最大化 coverage,而是把 population 從「place 有東西的 row」改成「能歸屬 city 的 menu」。
Inference 用量測,不用宣稱
place 不可用時,location 有時能透過 curated venue gazetteer 或 learned majority crosswalk 找回 city。Learned mapping 要求 support 至少 3、confidence 至少 0.75;會指向多個 city 的 generic venue name 會被 veto。
Held-out check 使用兩種信號都存在的 row:
| Inference path | Checked rows | Agreement |
|---|---|---|
| Curated gazetteer | 381 | 99.7% |
| Learned crosswalk,leave-one-out | 455 | 93.6% |
| Combined | 836 | 96.4% |
Caveat 不能省略:test row 有可 parse 的 place,可能比真正需要 inference 的 row 簡單。Measured agreement 是 evidence,不是 target population 的保證。
Constraint 保護 interpretation
八條 denial-style constraint 檢查 transit value 當 city、impossible date、missing ID、meal vocabulary、non-city place 與 derived value 的 provenance;cleaning 後 violation count 全部為零。
Missing date 與 invalid date 分開報告。Cleaning 前有 586 筆 missing date,真正 present 但超出範圍的只有六筆。把兩者混在一起會得到更戲劇化、但不誠實的 improvement。
這個 project 留下的原則
- 保存 raw field,interpretation 放 derived column。
- 相信 local pattern 前先做 global profile。
- Controlled vocabulary 是明確 analytical decision。
- 每筆進入或離開 answer set 的 row 都要 reconcile。
- 用 held-out evidence 測 inference,並說明 sample bias。
- Rule、threshold、test 與 provenance 都是 deliverable。
最後 pipeline 使用 standard-library Python 與 SQLite,配有 44 個 unit test 和 machine-readable audit report。工具選擇不如每個 cleaning decision 都能檢查、能重跑重要。
複習卡
- 從 analytical question 與 required grain 開始。
- 保存 raw data;另加 canonical value 與 provenance。
- Dataset-level ambiguity 可能擊敗 row-local rule。
- 品質變好時 coverage 合理地下降。
- 不只比 total,要 reconcile before/after population。
- Accuracy percentage 必須附 sampling caveat。