パーサー — アトミック化
最初の remediation 手順。アトミックでない限り、何も標準化してはいけません。
ADDR_STREET フィールドに「Na Budíně 854」が含まれている場合、これを標準化しても意味がありません — 住所レジストリは通り名と番地を別々に認識します。
パイプライン: 監査 → dq-parser → dq-standardizator → dq-adresar → dq-imputator → dq-deduplikator
ルール
- 元データは削除しない。 パース結果は新しいカラム(
_STD,_NUM1,_NUM2)に出力します。 元データは before/after 比較のための証拠であり、パーサーが誤った場合のセーフティネットです。 - 決定論的、ヒューリスティックではない。 3% のケースで誤った推測をするパーサーは、静かなデータ破壊を引き起こします。 ルールに確信が持てない場合は NULL を返し、その数をカウントします。 これは既知のパターンを持つ構造化フィールド(住所、氏名、日付)に適用されます。 パターンのない自由文テキストに対しては、統計的エンティティ認識(NER, spaCy)は正当なツールですが、 その出力は推定値であり、信頼度スコアとフラグが付随し、レビューなしにプロダクションカラムには移せません。
- パースの割合を測定する — 分解できた行数を記録します。これはレポートの指標です。
- パースは他のすべての前処理より先に行います。標準化、補完、重複除去はすべてアトミックなフィールドに依存します。
住所
最も一般的な欠陥: 番地用の専用カラムが 100% 空で、番地が通り名に混在している。
ソースでは Na Budíně 854, Nádražní 1211/25a, Bezručova 12/3 のようになります。
三つのコンポーネントに分解 — 通り名、家屋番号、方向番号(文字を含む場合あり):
-- MySQL 8: REGEXP_SUBSTR / REGEXP_REPLACE
UPDATE PARTY_ADDRESS SET
ADDR_NUM1_STD = REGEXP_SUBSTR(ADDR_STREET, '[0-9]+(?=(/[0-9]+[a-zA-Z]?)?[[:space:]]*$)'),
ADDR_NUM2_STD = REGEXP_SUBSTR(ADDR_STREET, '(?<=/)[0-9]+[a-zA-Z]?[[:space:]]*$'),
ADDR_STREET_STD = TRIM(REGEXP_REPLACE(ADDR_STREET, '[0-9]+(/[0-9]+[a-zA-Z]?)?[[:space:]]*$', ''));
Python 版(pandas パイプラインの場合):
num = raw.str.extract(r'(\d+)(?:/\d+\w?)?\s*$', expand=False) # 家屋番号
street = raw.str.replace(r'\d+(/\d+\w?)?\s*$', '', regex=True).str.strip()
数値の順序に注意: チェコ共和国では 家屋番号/方向番号 と記述されますが、
この規則が常に守られているわけではありません。
パーサーが一つの数値しか見つけられなかった場合は、家屋番号として保存し、方向番号は NULL のままにします — 推測しないでください。
市区町村フィールドの複合値
Praha 5 - Smíchov, České Budějovice - České Budějovice 5 — オペレーターが一つのフィールドに
住所階層全体(市区町村 + 行政区画 + 地区番号)を書き込んだものです。これにより、レジストリとの照合が不可能になります。
-- 最初のハイフン前 = 市区町村、残り = 行政区画
SELECT ADDR_CITY,
TRIM(SUBSTRING_INDEX(ADDR_CITY, '-', 1)) obec,
NULLIF(TRIM(SUBSTRING(ADDR_CITY, LOCATE('-',ADDR_CITY)+1)),'') mcast
FROM PARTY_ADDRESS WHERE ADDR_CITY LIKE '%-%';
パース開始前の候補検出:
SELECT ADDR_CITY, COUNT(*) cnt FROM PARTY_ADDRESS
WHERE ADDR_CITY REGEXP '[0-9]' OR ADDR_CITY LIKE '%-%' OR LENGTH(ADDR_CITY) > 50
GROUP BY 1 ORDER BY cnt DESC LIMIT 20;
注意: 市区町村名のハイフンは正当な場合もあります(Frýdek-Místek, Brandýs nad Labem-Stará Boleslav)。そのため、分解は市区町村レジストリに対して検証され、盲目的に行われません — そして、分解後に市区町村がレジストリに存在しない場合は、元の値を返します。
P.O. Box およびその他の範囲外の形式
P.O. Box、私書箱、「会社の配達住所」 — 形式的には有効な住所ですが、 建物レジストリには存在しません。これらを無理にパースせず、専用フラグでマークし、 マッチ率の指標から除外します(そうしないと、エラーではないものをエラーとして報告します)。
氏名
埋め込みスペース
D A V I D, Ž A N N A — すべての文字がスペースで区切られています。
すべてのトークンが一文字の場合、または一文字のトークンが過半数(残りは通常ハイフンやイニシャル)である場合に、
決定論的に結合できます:
toks = s.split()
if len(toks) > 1 and all(len(t) == 1 for t in toks):
s = "".join(toks)
elif len(toks) > 1 and sum(len(t) == 1 for t in toks) >= max(2, len(toks) - 1):
s = "".join(toks)
トークンを一般的に結合しないでください — Anna Marie は二つの名前であり、AnnaMarie ではありません。
複数部分からなる名前
名 + ミドルネームが一つのフィールドに含まれています。スペースで分割し、二番目のトークンを別に保存します。 一文字のトークン(イニシャル)は破棄します。そうしないと無意味なミドルネームが生成されます:
UPDATE PART_PARTY
SET PARTY_FNAME_STD = <proper>(SUBSTRING_INDEX(PARTY_FNAME,' ',1)),
PARTY_MNAME_STD = NULLIF(<proper>(SUBSTRING_INDEX(SUBSTRING_INDEX(PARTY_FNAME,' ',2),' ',-1)),
<proper>(SUBSTRING_INDEX(PARTY_FNAME,' ',1)))
WHERE PARTY_TYPE='P' AND LENGTH(SUBSTRING_INDEX(PARTY_FNAME,' ',2)) > 1;
名前フィールドに含まれる敬称
PARTY_NAME 内の Ing. Jan Novák。ピリオドではなく、最初のトークンが敬称辞書と一致するかで検出します —
J. Novák はイニシャルであり、敬称ではありません。
辞書は敬称コードリスト(REF_*_TITBEF は名前の前の敬称、TITAFT は名前の後)であり、
MuDr, MUDr., Mudr のようにデータ内で異なる表記が混在するため、ファジー一致を使用します。
パーサーは敬称を分離するだけです。正規形への変換は dq-standardizator の仕事です。
エンコーディングエラー
二つの異なる問題、二つの異なる解決策:
| エラー | 例 | 解決策 |
|---|---|---|
| 系統的な文字の置き換え | ĄUDOVÍT (正しくは ĽUDOVÍT) | パターンが一意であれば決定論的置換 — 例:単語の先頭の Ą は常に誤った書き換えであり、ポーランド語では決して出現しない |
| 欠落文字 | BE0ŠOVÁ (正しくは BENEŠOVÁ) | パーサーでは修正しない。 _CORRUPT フラグを付け、修復は dq-standardizator(氏名レジストリ)または dq-deduplikator(クラスター内のクリーンな二重行)に任せる |
s = re.sub(r"(^|(?<=\s))Ą", "Ľ", s) # 一意、決定論的
corrupt = bool(re.search(r"[0-9]", s)) # 名に数字 = 破損、別の場所で処理
この分割は意図的です。パーサーは確実なことだけを行います。欠落文字の再構築は推定であり、 検証可能な場所で処理します。
テキストとして保存された日付
パースする前に、フォーマットと日/月を確認します:
SELECT SUBSTRING(<col>,1,2) prvni, SUBSTRING(<col>,4,2) druhy, COUNT(*)
FROM <t> GROUP BY 1,2 ORDER BY 3 DESC LIMIT 20;
最初の位置が決して 12 を超えず、二番目の位置が超える場合、順序は MM.DD です。
両方が 12 を超える場合、フォーマットは混合しており、第二のソースに基づいて行ごとに判断する必要があります。
盲目的な STR_TO_DATE は静かなスワップを検出できず、テーブル全体に広げてしまいます。
パーサーの出力
- アトミックな値を持つ新しい
_STDカラム、元データはそのまま。 - 各コンポーネントのパース率(
空でない結果の行の %)。 - エラーテーブル: パーサーが分解できなかった行と、各行の理由 — 行が通過しなかった理由の監査証跡。 手動レビューまたは「これは範囲外」と判断するための入力。