Parser — Atomização
Primeiro passo da remediação. Não padronize nada que não esteja atômico. Padronizar o campo ADDR_STREET contendo "Na Budíně 854" não adianta — o registro de endereços conhece rua e número separadamente.
Pipeline: auditoria → analyzador → padronizador → catálogo → imputador → deduplicador.
Regras
- O original não é apagado. A saída analisada vai para novas colunas (
_STD,_NUM1,_NUM2). O original serve como evidência para análise antes/depois e como rede de segurança quando o parser errar. - Determinístico, não heurístico. Um parser que acerta 97 % das vezes mas erra 3 % causa danos silenciosos. Se não tiver certeza sobre uma regra, deixe NULL e conte quantos restaram. Vale para campos estruturados com padrão conhecido — endereço, nome, data. Para texto livre, onde padrão não existe, o reconhecimento estatístico de entidades (NER, spaCy) é uma ferramenta legítima, mas seu output é uma estimativa: acompanha um score de confiança e um flag, e não vai para coluna de produção sem revisão.
- Meça a taxa de parsing — quantas linhas foram decompostas. Essa é uma métrica para o relatório.
- Parse antes de qualquer outra coisa: padronização, imputação e deduplicação dependem de campos atômicos.
Endereço
Defeito mais comum: colunas dedicadas para números de imóveis estão 100 % vazias e o número está misturado no nome da rua. Na fonte aparece como Na Budíně 854, Nádražní 1211/25a, Bezručova 12/3.
Decomposição em três componentes — nome da rua, número predial, número orientador (com possível letra):
-- 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:]]*$', ''));
Equivalente em Python para pipeline pandas:
num = raw.str.extract(r'(\d+)(?:/\d+\w?)?\s*$', expand=False) # número predial
street = raw.str.replace(r'\d+(/\d+\w?)?\s*$', '', regex=True).str.strip()
Cuidado com a ordem dos números: na República Tcheca escreve-se predial/orientador, mas a convenção nem sempre é seguida. Se o parser encontrar apenas um número, armazene como predial e deixe o orientador NULL — não adivinhe.
Valor composto no campo de cidade
Praha 5 - Smíchov, České Budějovice - České Budějovice 5 — o operador escreveu toda a hierarquia de endereço (cidade + distrito + número do distrito) em um único campo. Impossibilita o pareamento com o registro.
-- primeiro segmento antes do traço = cidade, o restante = distrito
SELECT ADDR_CITY,
TRIM(SUBSTRING_INDEX(ADDR_CITY, '-', 1)) cidade,
NULLIF(TRIM(SUBSTRING(ADDR_CITY, LOCATE('-',ADDR_CITY)+1)),'') distrito
FROM PARTY_ADDRESS WHERE ADDR_CITY LIKE '%-%';
Detecção de candidatos antes de começar a analisar:
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;
Cuidado: o traço no nome da cidade às vezes é legítimo (Frýdek-Místek, Brandýs nad Labem-Stará Boleslav). Por isso, a decomposição é verificada contra o registro de cidades, não cegamente — e se após a decomposição a cidade não estiver no registro, retorne o valor original.
Caixa Postal e outras formas fora do escopo
Caixa Postal, apartado postal, "no endereço de entrega da empresa" — endereços formalmente válidos que o registro de imóveis não conhece. Não os analise à força; marque com um flag próprio e exclua da métrica de match rate (senão você reportará como erro algo que não é erro).
Nome
Espaços intercalados
D A V I D, Ž A N N A — cada letra separada por espaço. Pode ser colado deterministicamente se todas as tokens forem de uma letra, ou se a maioria for de uma letra (o restante normalmente é traço ou inicial):
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)
Nunca cole tokens genericamente — Anna Marie são dois nomes, não AnnaMarie.
Nome composto
Primeiro nome + nome do meio em um campo. Divida pelo espaço e armazene o segundo token separado; descarte tokens de uma letra (iniciais), senão você criará um nome do meio sem sentido:
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;
Título registrado no campo de nome
Ing. Jan Novák em PARTY_NAME. Detecte pela correspondência do primeiro token com o dicionário de títulos, não pelo ponto — J. Novák é inicial, não título.
O dicionário é a tabela de títulos (REF_*_TITBEF para títulos antes do nome, TITAFT depois) e a correspondência deve ser fuzzy, porque nos dados coexistem MuDr, MUDr. e Mudr. O parser apenas separa o título; a conversão para forma canônica é trabalho do padronizador.
Erros de codificação
Duas coisas diferentes, duas soluções diferentes:
| Erro | Exemplo | Solução |
|---|---|---|
| substituição sistemática de caractere | ĄUDOVÍT (deveria ser ĽUDOVÍT) | substituição determinística, se o padrão for inequívoco — por exemplo, Ą no início de palavra é sempre transcrição errada, porque em polonês nunca aparece ali |
| letra perdida | BE0ŠOVÁ (deveria ser BENEŠOVÁ) | não corrija no parser. Marque com flag _CORRUPT e deixe a recuperação para o padronizador (registro de nomes) ou deduplicador (gêmeo limpo no cluster) |
s = re.sub(r"(^|(?<=\s))Ą", "Ľ", s) # inequívoco, determinístico
corrupt = bool(re.search(r"[0-9]", s)) # dígitos no nome = corrompido, tratado em outra etapa
A divisão é intencional: o parser faz apenas o que é certo. A reconstrução de letra faltante é uma estimativa e pertence a uma etapa onde pode ser verificada contra algo.
Data armazenada como texto
Antes de analisar, verifique formato e dia/mês:
SELECT SUBSTRING(<col>,1,2) primeiro, SUBSTRING(<col>,4,2) segundo, COUNT(*)
FROM <t> GROUP BY 1,2 ORDER BY 3 DESC LIMIT 20;
Se a primeira posição nunca ultrapassa 12 e a segunda sim, a ordem é MM.DD; se ambas ultrapassam 12, o formato é misto e é necessário decidir por linha com base em outra fonte. O cego STR_TO_DATE não detecta troca silenciosa e a espalha por toda a tabela.
Saída do parser
- Novas colunas
_STDcom valores atômicos, originais intactos. - Taxa de parsing para cada componente (
% de linhas com resultado não vazio). - Tabela de erros com linhas que o parser não decompôs, com o motivo de cada uma — trilha de auditoria sobre por que o registro não passou. Entrada para revisão manual ou para decisão "isto está fora do escopo".