HOWTO · Python
PythonのSoundex
Python で Soundex の音声符号化アルゴリズムを実装し、英語の名前を比較する方法を説明します。このアルゴリズムは、先頭の英字1文字とそれに続く数字3桁からなる4文字のコードを生成します。
このページの内容
Soundex は、英語の名前を索引付けするための音声符号化アルゴリズムです。名前を、先頭の英字1文字とそれに続く数字3桁からなる4文字のコードに変換します。同じコードになる名前は、このアルゴリズム上で似た響きになることがあり、綴りの異なる名前を検索する手がかりになります。ただし、コードが一致しても候補が見つかったにすぎず、2つの名前が同一であることや、発音が同じであることを証明するものではありません。
Python には組み込みの soundex 関数はありません。以下のコードでは、一般的なアメリカ式 Soundex の規則を実装し、各手順を確認して応用できるようにしています。
Soundex の役割
Soundex は名前の先頭文字を残し、それ以降の子音を大まかな音のグループに応じて数字に置き換えます。隣り合う重複コードを取り除き、短い結果にはゼロを補い、長い結果は数字3桁に切り詰めます。先頭文字とその数字を合わせたものが最終的なコードです。
たとえば、よく知られた Robert と Rupert の組み合わせは、この規則ではどちらも R163 になります。この符号化は意図的に簡潔で、綴りの異なる名前が同じキーになるよう細かな情報を省きます。完全一致や人による確認の代わりではなく、検索を補助する手段の一つとして役立ちます。
Python で Soundex を実装する
この実装は、ASCII の英字を少なくとも1文字含む文字列を受け付けます。空白、句読点、その他の非 ASCII 文字は無視し、英字の大文字・小文字を統一します。入力が文字列でない場合、または ASCII の英字を含まない場合は、明確なメッセージを伴う ValueError を送出します。コードでは先頭の英字を接頭部として残し、それ以降の英字を対応表で変換します。
次の例を実行してください。
このセクションのコードと表示されるラベルや出力は英語のままです。
name = "Robert"
letters = [char.upper() for char in name if ("A" <= char <= "Z") or ("a" <= char <= "z")]
groups = {
**dict.fromkeys("BFPV", "1"),
**dict.fromkeys("CGJKQSXZ", "2"),
**dict.fromkeys("DT", "3"),
"L": "4",
**dict.fromkeys("MN", "5"),
"R": "6",
}
previous = groups.get(letters[0])
digits = []
for letter in letters[1:]:
code = groups.get(letter)
if code is None:
if letter in "AEIOUY":
previous = None
continue
if code != previous:
digits.append(code)
previous = code
print(letters[0] + "".join(digits)[:3].ljust(3, "0"))
R163
プログラムは最初の ASCII 英字を残し、対応する数字を最大3桁まで加え、短い結果にはゼロを補います。以下の完全な関数では、符号化の前に入力も検査します。
def soundex(name):
"""Return a four-character Soundex code for an ASCII-letter name."""
if not isinstance(name, str):
raise ValueError("name must be a string")
letters = [
char.upper()
for char in name
if ("A" <= char <= "Z") or ("a" <= char <= "z")
]
if not letters:
raise ValueError("name must contain an ASCII letter")
groups = {
**dict.fromkeys("BFPV", "1"),
**dict.fromkeys("CGJKQSXZ", "2"),
**dict.fromkeys("DT", "3"),
"L": "4",
**dict.fromkeys("MN", "5"),
"R": "6",
}
first = letters[0]
previous = groups.get(first)
digits = []
for letter in letters[1:]:
code = groups.get(letter)
if code is None:
# Vowels and Y separate equal codes; H and W do not.
if letter in "AEIOUY":
previous = None
continue
if code != previous:
digits.append(code)
previous = code
return first + "".join(digits)[:3].ljust(3, "0")
print(soundex("Robert"))
R163
大文字に変換する前に、元の文字をフィルターしてください。これにより、ß のように大文字化すると ASCII の英字に展開される Unicode 文字が誤って対象になるのを防ぎます。小文字の ASCII 英字は引き続き正規化されます。文字列でない入力や ASCII の英字を含まない文字列では、ValueError が送出されます。
Soundex の規則を理解する
子音のグループは BFPV → 1、CGJKQSXZ → 2、DT → 3、L → 4、MN → 5、R → 6 です。母音と Y は数字を追加しません。これらは同じ子音コードを区切るため、その前後にある同じ音はそれぞれコードに反映されることがあります。H と W は無視されますが、コードを区切りません。
先頭文字は子音グループにも含まれる場合でも、そのまま保持されます。同じ文字を二重に数えないよう、実装では先頭文字に対応するコードを直前のコードとして初期化します。たとえば Pfister の先頭の P とその次の F はどちらも 1 に対応するため、隣接するこのグループから出力される 1 は一つだけです。名前の処理後、数字は先頭から3桁だけ残し、足りない桁はゼロで埋めます。
対象になるのは ASCII の英字だけです。そのため、この実装では空白や句読点は隣接するコードの並びを区切りません。これは明示的な正規化方針です。アポストロフィ、アクセント付き文字、その他の文字体系に別の扱いが必要なら、検索にこのコードを使う前に、その方針を定めてテストしてください。
実装をテストする
一致する名前の組み合わせと、H/W の規則を確認できる名前を使って、関数全体を実行します。
このセクションのコードと表示されるラベルや出力は英語のままです。
def soundex(name):
"""Return a four-character Soundex code for an ASCII-letter name."""
if not isinstance(name, str):
raise ValueError("name must be a string")
letters = [
char.upper()
for char in name
if ("A" <= char <= "Z") or ("a" <= char <= "z")
]
if not letters:
raise ValueError("name must contain an ASCII letter")
groups = {
**dict.fromkeys("BFPV", "1"),
**dict.fromkeys("CGJKQSXZ", "2"),
**dict.fromkeys("DT", "3"),
"L": "4",
**dict.fromkeys("MN", "5"),
"R": "6",
}
first = letters[0]
previous = groups.get(first)
digits = []
for letter in letters[1:]:
code = groups.get(letter)
if code is None:
if letter in "AEIOUY":
previous = None
continue
if code != previous:
digits.append(code)
previous = code
return first + "".join(digits)[:3].ljust(3, "0")
for name in ("Robert", "Rupert", "Ashcraft", "Pfister"):
print(f"{name}: {soundex(name)}")
Robert: R163
Rupert: R163
Ashcraft: A261
Pfister: P236
Robert と Rupert は同じコードになります。Ashcraft では H が同じ子音コードを区切らないことを、Pfister では先頭の子音が F との重複抑制に関わることを確認できます。
プロジェクトでは、代表的な名前を使い、採用する Soundex の方式で期待されるコードをアサーションとして指定してください。各子音グループで始まる名前、同じ子音グループの間に母音がある名前、句読点を含む名前、ASCII の英字を含まない入力を含めます。入力検査は無効な入力に対して ValueError を送出します。アプリケーションで役立つメッセージを表示したり、レコードをスキップしたりできる箇所で、その例外を処理してください。どの文字を対象にするかを変えるとコードも変わるため、これらの検査はアプリケーション独自の正規化規則と合わせてください。
Soundex の限界
Soundex は英語の発音と綴りの慣習を前提に設計されています。大まかなグループ分けのため、人が似ていると感じる名前を区別してしまうことも、文脈上は似ていない名前に同じコードを付けることもあります。発音は言語、地域、個人によって異なりますが、この実装が扱うのは綴りだけです。
4文字のコードには衝突もあります。限られた数のコードを多くの異なる名前で共有するためです。検索対象を絞るために使い、その後、より詳しい記録と照合するか、人に一致を確認してもらってください。多言語のデータには、そのデータセットの言語や名前に適した方法を検討し、結果を再現できるよう、正確な正規化方針と Soundex の方式を記録してください。