@@ -38,13 +38,19 @@ def __init__(self, path: str = _DATA) -> None:
3838 import pyarrow .parquet as pq # local import: optional dependency
3939
4040 tbl = pq .read_table (
41- path , columns = ["name" , "name_ascii" , "country_code" , "gender" , "frequency" ]
41+ path ,
42+ columns = [
43+ "name" , "name_ascii" , "country_code" , "gender" ,
44+ "frequency" , "country_share" , "phonetic" ,
45+ ],
4246 )
4347 names = tbl .column ("name" ).to_pylist ()
4448 asciis = tbl .column ("name_ascii" ).to_pylist ()
4549 ccs = tbl .column ("country_code" ).to_pylist ()
4650 genders = tbl .column ("gender" ).to_pylist ()
4751 freqs = tbl .column ("frequency" ).to_pylist ()
52+ shares = tbl .column ("country_share" ).to_pylist ()
53+ phonetics = tbl .column ("phonetic" ).to_pylist ()
4854
4955 # (country, gender) -> (names[], cumulative_weights[]).
5056 # Weights are *relative* frequencies (shares), never raw counts.
@@ -53,12 +59,21 @@ def __init__(self, path: str = _DATA) -> None:
5359 gender_by : Dict [Tuple [str , Optional [str ]], Dict [str , float ]] = {}
5460 # name_key -> {country: summed within-country share} for origin detection
5561 country_by : Dict [str , Dict [str , float ]] = {}
62+ # (name_key, country) -> phonetic key; (country, phonetic) -> {name: share}
63+ phon_key : Dict [Tuple [str , str ], str ] = {}
64+ homo : Dict [Tuple [str , str ], Dict [str , float ]] = {}
5665
5766 acc : Dict [Tuple [Optional [str ], str ], List ] = {}
58- for name , asc , cc , g , fr in zip (names , asciis , ccs , genders , freqs ):
67+ for name , asc , cc , g , fr , cs , phon in zip (
68+ names , asciis , ccs , genders , freqs , shares , phonetics
69+ ):
5970 if not g or g == " " :
6071 continue
6172 w = float (fr ) if fr else 1e-6
73+ csh = float (cs ) if cs else 1e-9
74+ if cc and phon :
75+ homo .setdefault ((cc , phon ), {})
76+ homo [(cc , phon )][name ] = homo [(cc , phon )].get (name , 0.0 ) + csh
6277 for scope in (cc , None ): # per-country and global pools
6378 key = (scope , g )
6479 lst = acc .setdefault (key , [[], []])
@@ -72,6 +87,8 @@ def __init__(self, path: str = _DATA) -> None:
7287 if cc :
7388 c = country_by .setdefault (akey , {})
7489 c [cc ] = c .get (cc , 0.0 ) + w
90+ if phon :
91+ phon_key [(akey , cc )] = phon
7592
7693 # finalize cumulative weights for O(log n) weighted draw
7794 for key , (nm , wt ) in acc .items ():
@@ -84,6 +101,8 @@ def __init__(self, path: str = _DATA) -> None:
84101 self ._pools = pools
85102 self ._gender_by = gender_by
86103 self ._country_by = country_by
104+ self ._phon_key = phon_key
105+ self ._homo = homo
87106
88107 def infer (self , name : str , country : Optional [str ] = None ) -> Optional [str ]:
89108 key = name .strip ().lower ()
@@ -119,6 +138,29 @@ def draw(self, country: Optional[str], gender: str, avoid: Optional[str] = None)
119138 def countries (self ) -> List [str ]:
120139 return sorted ({cc for (cc , _g ) in self ._pools if cc })
121140
141+ def homophones (
142+ self , name : str , country : str , top : int = 10 , include_self : bool = True
143+ ) -> List [Tuple [str , float ]]:
144+ """Names that sound like ``name`` in ``country``, with probabilities.
145+
146+ Groups names by phonetic key (double-metaphone) within the country and
147+ weights each by its country-wide frequency share. Returns
148+ ``[(name, probability)]`` (probabilities over the group sum to 1),
149+ highest first.
150+ """
151+ phon = self ._phon_key .get ((name .strip ().lower (), country ))
152+ if not phon :
153+ return []
154+ group = self ._homo .get ((country , phon ))
155+ if not group :
156+ return []
157+ items = list (group .items ())
158+ if not include_self :
159+ items = [(n , w ) for n , w in items if n .lower () != name .strip ().lower ()]
160+ total = sum (w for _n , w in items ) or 1.0
161+ ranked = sorted (items , key = lambda kv : kv [1 ], reverse = True )
162+ return [(n , w / total ) for n , w in ranked [:top ]]
163+
122164 def detect_country (self , name : str , top : int = 5 ) -> List [Tuple [str , float ]]:
123165 """Rank the countries where ``name`` is most characteristic.
124166
@@ -158,6 +200,20 @@ def detect_country(name: str, top: int = 5) -> List[Tuple[str, float]]:
158200 return _bank ().detect_country (name , top )
159201
160202
203+ def homophones (
204+ name : str , country : str , top : int = 10 , include_self : bool = True
205+ ) -> List [Tuple [str , float ]]:
206+ """Same-sounding names in a country, with probabilities.
207+
208+ homophones("Dominique", "FR")
209+ # [("Dominique", 0.91), ("Dominic", 0.03), ("Dominik", 0.02), ...]
210+
211+ Groups by double-metaphone within ``country`` and weights by country-wide
212+ frequency share. Probabilities sum to 1. ``[]`` if the name is unknown.
213+ """
214+ return _bank ().homophones (name , country .upper () if country else "" , top , include_self )
215+
216+
161217def first_name (country : Optional [str ] = None , gender : str = MALE ) -> Optional [str ]:
162218 """Frequency-weighted first name for a country + gender."""
163219 return _bank ().draw (country .upper () if country else None , gender )
0 commit comments