instate 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- instate/__init__.py +35 -0
- instate/_utils.py +357 -0
- instate/constants.py +127 -0
- instate/data/instate_unique_ln_state_prop_v1.csv.gz +0 -0
- instate/data/lastname_langs_india.csv.tar.gz +0 -0
- instate/data/lastname_langs_india_top3.csv.tar.gz +0 -0
- instate/data/state_lang_labels.pt +0 -0
- instate/data/state_to_languages.csv +36 -0
- instate/electoral.py +141 -0
- instate/nnets.py +46 -0
- instate/predict.py +235 -0
- instate-1.0.0.dist-info/METADATA +226 -0
- instate-1.0.0.dist-info/RECORD +14 -0
- instate-1.0.0.dist-info/WHEEL +4 -0
instate/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
instate: Predict state and language from Indian lastnames.
|
|
3
|
+
|
|
4
|
+
This package provides functions to:
|
|
5
|
+
1. Look up state distributions from 2017 Indian electoral rolls
|
|
6
|
+
2. Predict states and languages using neural networks
|
|
7
|
+
|
|
8
|
+
Main functions:
|
|
9
|
+
- get_state_distribution: Get P(state|lastname) from electoral rolls
|
|
10
|
+
- get_state_languages: Map states to official languages
|
|
11
|
+
- predict_state: Neural prediction of most likely states
|
|
12
|
+
- predict_language: Neural prediction of most likely languages
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .electoral import (
|
|
16
|
+
get_state_distribution,
|
|
17
|
+
get_state_languages,
|
|
18
|
+
list_available_states,
|
|
19
|
+
)
|
|
20
|
+
from .predict import predict_language, predict_state
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"get_state_distribution",
|
|
24
|
+
"get_state_languages",
|
|
25
|
+
"predict_state",
|
|
26
|
+
"predict_language",
|
|
27
|
+
"list_available_states",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
from importlib.metadata import version
|
|
32
|
+
|
|
33
|
+
__version__ = version("instate")
|
|
34
|
+
except ImportError:
|
|
35
|
+
__version__ = "1.0.0"
|
instate/_utils.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Internal utilities for instate package.
|
|
3
|
+
|
|
4
|
+
This module contains helper functions for data loading, name cleaning, and caching.
|
|
5
|
+
These are internal utilities and not part of the public API.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
import pandas as pd
|
|
13
|
+
import requests
|
|
14
|
+
import torch
|
|
15
|
+
import torch.nn as nn
|
|
16
|
+
from tqdm import tqdm
|
|
17
|
+
|
|
18
|
+
# Cache for loaded data
|
|
19
|
+
_CACHE = {}
|
|
20
|
+
|
|
21
|
+
# URLs for downloading data
|
|
22
|
+
ELECTORAL_DATA_URLS = {
|
|
23
|
+
"v1": "https://github.com/appeler/instate/raw/main/data/instate_unique_ln_state_prop_v1.csv.gz",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
MODEL_URLS = {
|
|
27
|
+
"gru": "https://dataverse.harvard.edu/api/v1/access/datafile/6981460",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def prepare_name_dataframe(
|
|
32
|
+
names: pd.DataFrame | list[str], name_column: str | None = None
|
|
33
|
+
) -> pd.DataFrame:
|
|
34
|
+
"""Convert input to DataFrame with standardized name column.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
names: DataFrame or list of names
|
|
38
|
+
name_column: Column name if DataFrame provided
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
DataFrame with names in first column
|
|
42
|
+
"""
|
|
43
|
+
if isinstance(names, list):
|
|
44
|
+
return pd.DataFrame({"name": names})
|
|
45
|
+
|
|
46
|
+
df = names.copy()
|
|
47
|
+
|
|
48
|
+
# If no column specified, try to find one
|
|
49
|
+
if name_column is None:
|
|
50
|
+
# Look for common name columns
|
|
51
|
+
possible_cols = [
|
|
52
|
+
c
|
|
53
|
+
for c in df.columns
|
|
54
|
+
if any(n in c.lower() for n in ["name", "lastname", "surname"])
|
|
55
|
+
]
|
|
56
|
+
if not possible_cols:
|
|
57
|
+
# Just use first column
|
|
58
|
+
name_column = df.columns[0]
|
|
59
|
+
else:
|
|
60
|
+
name_column = possible_cols[0]
|
|
61
|
+
|
|
62
|
+
# Ensure the name column is first
|
|
63
|
+
if name_column != df.columns[0]:
|
|
64
|
+
cols = [name_column] + [c for c in df.columns if c != name_column]
|
|
65
|
+
df = df[cols]
|
|
66
|
+
|
|
67
|
+
return df
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def clean_name(name: str) -> str:
|
|
71
|
+
"""Clean and standardize a single name.
|
|
72
|
+
|
|
73
|
+
- Convert to lowercase
|
|
74
|
+
- Strip whitespace
|
|
75
|
+
- Remove non-alphabetic characters
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
name: Input name string
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Cleaned name
|
|
82
|
+
"""
|
|
83
|
+
if not isinstance(name, str):
|
|
84
|
+
return ""
|
|
85
|
+
|
|
86
|
+
# Basic cleaning
|
|
87
|
+
cleaned = name.strip().lower()
|
|
88
|
+
|
|
89
|
+
# Keep only alphabetic characters
|
|
90
|
+
cleaned = "".join(c for c in cleaned if c.isalpha())
|
|
91
|
+
|
|
92
|
+
return cleaned
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def clean_names_in_df(df: pd.DataFrame, name_column: str) -> pd.DataFrame:
|
|
96
|
+
"""Clean names in a DataFrame column.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
df: Input DataFrame
|
|
100
|
+
name_column: Column containing names
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
DataFrame with added __cleaned_name column and filtered rows
|
|
104
|
+
"""
|
|
105
|
+
result = df.copy()
|
|
106
|
+
|
|
107
|
+
# Handle empty DataFrame
|
|
108
|
+
if len(result) == 0:
|
|
109
|
+
result["__cleaned_name"] = pd.Series([], dtype=str)
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
# Clean names
|
|
113
|
+
result["__cleaned_name"] = result[name_column].apply(clean_name)
|
|
114
|
+
|
|
115
|
+
# Filter out invalid names
|
|
116
|
+
result = result[result["__cleaned_name"].str.len() > 2]
|
|
117
|
+
|
|
118
|
+
# Drop duplicates based on cleaned name
|
|
119
|
+
result = result.drop_duplicates(subset=["__cleaned_name"], keep="first")
|
|
120
|
+
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_app_file_path(filename: str) -> str:
|
|
125
|
+
"""Get path for cached application data.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
filename: Name of file
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Full path to file in app data directory
|
|
132
|
+
"""
|
|
133
|
+
user_dir = os.path.expanduser("~")
|
|
134
|
+
app_data_dir = os.path.join(user_dir, ".instate")
|
|
135
|
+
|
|
136
|
+
if not os.path.exists(app_data_dir):
|
|
137
|
+
os.makedirs(app_data_dir)
|
|
138
|
+
|
|
139
|
+
return os.path.join(app_data_dir, filename)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def download_file(url: str, target: str) -> bool:
|
|
143
|
+
"""Download file with progress bar.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
url: URL to download from
|
|
147
|
+
target: Target file path
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
True if successful, False otherwise
|
|
151
|
+
"""
|
|
152
|
+
try:
|
|
153
|
+
r = requests.get(url, stream=True)
|
|
154
|
+
|
|
155
|
+
if r.status_code == 200:
|
|
156
|
+
chunk_size = 64 * 1024
|
|
157
|
+
total_size = int(r.headers.get("content-length", 0)) / chunk_size + 1
|
|
158
|
+
|
|
159
|
+
with open(target, "wb") as f:
|
|
160
|
+
for data in tqdm(
|
|
161
|
+
r.iter_content(chunk_size),
|
|
162
|
+
total=round(total_size, 1),
|
|
163
|
+
unit_scale=chunk_size / 1024,
|
|
164
|
+
unit="KB",
|
|
165
|
+
desc="Downloading",
|
|
166
|
+
):
|
|
167
|
+
f.write(data)
|
|
168
|
+
return True
|
|
169
|
+
else:
|
|
170
|
+
print(f"Download failed with status code: {r.status_code}")
|
|
171
|
+
return False
|
|
172
|
+
except Exception as e:
|
|
173
|
+
print(f"Download error: {e}")
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def load_electoral_data(dataset: str = "v1") -> pd.DataFrame:
|
|
178
|
+
"""Load electoral rolls data, downloading if needed.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
dataset: Dataset version to load
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
DataFrame with electoral rolls data
|
|
185
|
+
"""
|
|
186
|
+
global _CACHE
|
|
187
|
+
|
|
188
|
+
cache_key = f"electoral_{dataset}"
|
|
189
|
+
if cache_key in _CACHE:
|
|
190
|
+
return _CACHE[cache_key]
|
|
191
|
+
|
|
192
|
+
# Check if data exists locally
|
|
193
|
+
filename = f"instate_unique_ln_state_prop_{dataset}.csv.gz"
|
|
194
|
+
data_path = get_app_file_path(filename)
|
|
195
|
+
|
|
196
|
+
if not os.path.exists(data_path):
|
|
197
|
+
# First try to find the file in the package data directory
|
|
198
|
+
package_data_dir = os.path.join(os.path.dirname(__file__), "data")
|
|
199
|
+
local_path = os.path.join(package_data_dir, filename)
|
|
200
|
+
|
|
201
|
+
if os.path.exists(local_path):
|
|
202
|
+
print("Copying electoral rolls data from package...")
|
|
203
|
+
import shutil
|
|
204
|
+
|
|
205
|
+
shutil.copy2(local_path, data_path)
|
|
206
|
+
else:
|
|
207
|
+
print(f"Downloading electoral rolls data ({dataset})...")
|
|
208
|
+
if not download_file(ELECTORAL_DATA_URLS[dataset], data_path):
|
|
209
|
+
raise RuntimeError("Failed to download electoral data")
|
|
210
|
+
|
|
211
|
+
# Load data
|
|
212
|
+
df = pd.read_csv(data_path)
|
|
213
|
+
df.rename(columns={"last_name": "__last_name"}, inplace=True)
|
|
214
|
+
|
|
215
|
+
# Cache it
|
|
216
|
+
_CACHE[cache_key] = df
|
|
217
|
+
|
|
218
|
+
return df
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def load_gru_model():
|
|
222
|
+
"""Load GRU model for state prediction.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Loaded PyTorch model
|
|
226
|
+
"""
|
|
227
|
+
global _CACHE
|
|
228
|
+
|
|
229
|
+
if "gru_model" in _CACHE:
|
|
230
|
+
return _CACHE["gru_model"]
|
|
231
|
+
|
|
232
|
+
# Check if model exists
|
|
233
|
+
model_path = get_app_file_path("instate_gru.pth")
|
|
234
|
+
|
|
235
|
+
if not os.path.exists(model_path):
|
|
236
|
+
print("Downloading GRU model...")
|
|
237
|
+
if not download_file(MODEL_URLS["gru"], model_path):
|
|
238
|
+
raise RuntimeError("Failed to download GRU model")
|
|
239
|
+
|
|
240
|
+
# Load model
|
|
241
|
+
from .constants import GRU_HIDDEN_SIZE, GRU_N_LETTERS, GT_KEYS
|
|
242
|
+
from .nnets import GRU_net
|
|
243
|
+
|
|
244
|
+
device = torch.device("cpu")
|
|
245
|
+
model = GRU_net(GRU_N_LETTERS, GRU_HIDDEN_SIZE, len(GT_KEYS))
|
|
246
|
+
model.load_state_dict(torch.load(model_path, map_location=device))
|
|
247
|
+
model.eval()
|
|
248
|
+
|
|
249
|
+
_CACHE["gru_model"] = model
|
|
250
|
+
|
|
251
|
+
return model
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def load_lstm_model():
|
|
255
|
+
"""Load LSTM model for language prediction.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Loaded PyTorch model and supporting data
|
|
259
|
+
"""
|
|
260
|
+
global _CACHE
|
|
261
|
+
|
|
262
|
+
if "lstm_model" in _CACHE:
|
|
263
|
+
return _CACHE["lstm_model"], _CACHE["lstm_data"]
|
|
264
|
+
|
|
265
|
+
# Import constants instead of loading from files
|
|
266
|
+
from .constants import (
|
|
267
|
+
CHAR_TO_IDX,
|
|
268
|
+
IDX_TO_LANG,
|
|
269
|
+
NUM_LANGUAGES,
|
|
270
|
+
VOCAB_SIZE,
|
|
271
|
+
)
|
|
272
|
+
# LanguagePredictor is now defined in this file
|
|
273
|
+
|
|
274
|
+
# Model configuration
|
|
275
|
+
embedding_dim = 50
|
|
276
|
+
hidden_dim = 128
|
|
277
|
+
|
|
278
|
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
279
|
+
model = LanguagePredictor(VOCAB_SIZE, embedding_dim, hidden_dim, NUM_LANGUAGES)
|
|
280
|
+
model.to(device)
|
|
281
|
+
|
|
282
|
+
# Load weights
|
|
283
|
+
data_dir = os.path.dirname(__file__)
|
|
284
|
+
model_file = os.path.join(data_dir, "data", "state_lang_labels.pt")
|
|
285
|
+
if torch.cuda.is_available():
|
|
286
|
+
model.load_state_dict(torch.load(model_file))
|
|
287
|
+
else:
|
|
288
|
+
model.load_state_dict(torch.load(model_file, map_location=torch.device("cpu")))
|
|
289
|
+
|
|
290
|
+
model.eval()
|
|
291
|
+
|
|
292
|
+
# Cache everything
|
|
293
|
+
lstm_data = {"char2idx": CHAR_TO_IDX, "idx2lang": IDX_TO_LANG, "device": device}
|
|
294
|
+
|
|
295
|
+
_CACHE["lstm_model"] = model
|
|
296
|
+
_CACHE["lstm_data"] = lstm_data
|
|
297
|
+
|
|
298
|
+
return model, lstm_data
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def load_language_lookup_data():
|
|
302
|
+
"""Load data for KNN language lookup.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
DataFrame with lastname to language mapping
|
|
306
|
+
"""
|
|
307
|
+
import tarfile
|
|
308
|
+
|
|
309
|
+
global _CACHE
|
|
310
|
+
|
|
311
|
+
if "lang_lookup" in _CACHE:
|
|
312
|
+
return _CACHE["lang_lookup"]
|
|
313
|
+
|
|
314
|
+
data_file_name = "lastname_langs_india"
|
|
315
|
+
data_path = get_app_file_path(data_file_name)
|
|
316
|
+
|
|
317
|
+
if not os.path.exists(data_path + ".csv"):
|
|
318
|
+
data_dir = os.path.dirname(__file__)
|
|
319
|
+
gz_path = os.path.join(data_dir, "data", f"{data_file_name}.csv.tar.gz")
|
|
320
|
+
print("Extracting language lookup data...")
|
|
321
|
+
with tarfile.open(gz_path, "r:gz") as tar:
|
|
322
|
+
tar.extract(f"{data_file_name}.csv", data_path, filter="data")
|
|
323
|
+
|
|
324
|
+
df = pd.read_csv(f"{data_path}/{data_file_name}.csv")
|
|
325
|
+
_CACHE["lang_lookup"] = df
|
|
326
|
+
|
|
327
|
+
return df
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class LanguagePredictor(nn.Module):
|
|
331
|
+
"""LSTM model for predicting languages from names.
|
|
332
|
+
|
|
333
|
+
This model uses character embeddings and LSTM to predict the top 3 most
|
|
334
|
+
likely languages for a given name.
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
def __init__(
|
|
338
|
+
self, num_chars, embedding_dim=64, lstm_hidden_dim=128, num_languages=37
|
|
339
|
+
):
|
|
340
|
+
super().__init__()
|
|
341
|
+
self.embedding = nn.Embedding(num_chars, embedding_dim)
|
|
342
|
+
self.lstm = nn.LSTM(embedding_dim, lstm_hidden_dim, batch_first=True)
|
|
343
|
+
self.fc1 = nn.Linear(lstm_hidden_dim, num_languages)
|
|
344
|
+
self.fc2 = nn.Linear(lstm_hidden_dim, num_languages)
|
|
345
|
+
self.fc3 = nn.Linear(lstm_hidden_dim, num_languages)
|
|
346
|
+
|
|
347
|
+
def forward(self, x, lengths):
|
|
348
|
+
x = self.embedding(x)
|
|
349
|
+
x = nn.utils.rnn.pack_padded_sequence(
|
|
350
|
+
x, lengths, batch_first=True, enforce_sorted=False
|
|
351
|
+
)
|
|
352
|
+
_, (h_n, _) = self.lstm(x)
|
|
353
|
+
h_n = h_n.squeeze(0)
|
|
354
|
+
out1 = self.fc1(h_n)
|
|
355
|
+
out2 = self.fc2(h_n)
|
|
356
|
+
out3 = self.fc3(h_n)
|
|
357
|
+
return out1, out2, out3
|
instate/constants.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Constants for the instate package.
|
|
3
|
+
|
|
4
|
+
Contains static data that was previously stored in small files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
# Language list (originally from langs.txt)
|
|
10
|
+
LANGUAGES = [
|
|
11
|
+
"sindhi",
|
|
12
|
+
"nepali",
|
|
13
|
+
"kannada",
|
|
14
|
+
"marathi",
|
|
15
|
+
"mizo",
|
|
16
|
+
"adi",
|
|
17
|
+
"garo",
|
|
18
|
+
"tagin",
|
|
19
|
+
"assamese",
|
|
20
|
+
"hindi",
|
|
21
|
+
"odia",
|
|
22
|
+
"french",
|
|
23
|
+
"punjabi",
|
|
24
|
+
"naga languages",
|
|
25
|
+
"english",
|
|
26
|
+
"chenchu",
|
|
27
|
+
"urdu",
|
|
28
|
+
"bengali",
|
|
29
|
+
"maithili",
|
|
30
|
+
"dogri",
|
|
31
|
+
"kokborok",
|
|
32
|
+
"santali",
|
|
33
|
+
"kashmiri",
|
|
34
|
+
"gujarati",
|
|
35
|
+
"apatani",
|
|
36
|
+
"tulu",
|
|
37
|
+
"konkani",
|
|
38
|
+
"telugu",
|
|
39
|
+
"malayalam",
|
|
40
|
+
"tamil",
|
|
41
|
+
"meitei",
|
|
42
|
+
"khasi",
|
|
43
|
+
"gondi",
|
|
44
|
+
"bodo",
|
|
45
|
+
"nishi",
|
|
46
|
+
"chakma",
|
|
47
|
+
"pahari and kumauni",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# Character to index mapping (originally from char2idx.json)
|
|
51
|
+
CHAR_TO_IDX = {
|
|
52
|
+
"<PAD>": 0,
|
|
53
|
+
"n": 1,
|
|
54
|
+
"g": 2,
|
|
55
|
+
"i": 3,
|
|
56
|
+
"m": 4,
|
|
57
|
+
"c": 5,
|
|
58
|
+
"w": 6,
|
|
59
|
+
"u": 7,
|
|
60
|
+
"e": 8,
|
|
61
|
+
"v": 9,
|
|
62
|
+
"d": 10,
|
|
63
|
+
"a": 11,
|
|
64
|
+
"l": 12,
|
|
65
|
+
"t": 13,
|
|
66
|
+
"s": 14,
|
|
67
|
+
"q": 15,
|
|
68
|
+
"b": 16,
|
|
69
|
+
"f": 17,
|
|
70
|
+
"o": 18,
|
|
71
|
+
"z": 19,
|
|
72
|
+
"p": 20,
|
|
73
|
+
"r": 21,
|
|
74
|
+
"k": 22,
|
|
75
|
+
"h": 23,
|
|
76
|
+
"y": 24,
|
|
77
|
+
"x": 25,
|
|
78
|
+
"j": 26,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Derived mappings for convenience
|
|
82
|
+
LANG_TO_IDX = {lang: idx for idx, lang in enumerate(LANGUAGES)}
|
|
83
|
+
IDX_TO_LANG = {idx: lang for lang, idx in LANG_TO_IDX.items()}
|
|
84
|
+
|
|
85
|
+
# Model dimensions
|
|
86
|
+
VOCAB_SIZE = len(CHAR_TO_IDX)
|
|
87
|
+
NUM_LANGUAGES = len(LANGUAGES)
|
|
88
|
+
|
|
89
|
+
# GRU model constants for state prediction
|
|
90
|
+
GT_KEYS = [
|
|
91
|
+
"Andaman and Nicobar Islands",
|
|
92
|
+
"Andhra Pradesh",
|
|
93
|
+
"Arunachal Pradesh",
|
|
94
|
+
"Assam",
|
|
95
|
+
"Bihar",
|
|
96
|
+
"Chandigarh",
|
|
97
|
+
"Dadra and Nagar Haveli",
|
|
98
|
+
"Daman and Diu",
|
|
99
|
+
"Delhi",
|
|
100
|
+
"Goa",
|
|
101
|
+
"Gujarat",
|
|
102
|
+
"Haryana",
|
|
103
|
+
"Jharkhand",
|
|
104
|
+
"Jammu and Kashmir and Ladakh",
|
|
105
|
+
"Karnataka",
|
|
106
|
+
"Kerala",
|
|
107
|
+
"Maharashtra",
|
|
108
|
+
"Manipur",
|
|
109
|
+
"Meghalaya",
|
|
110
|
+
"Mizoram",
|
|
111
|
+
"Madhya Pradesh",
|
|
112
|
+
"Nagaland",
|
|
113
|
+
"Odisha",
|
|
114
|
+
"Puducherry",
|
|
115
|
+
"Punjab",
|
|
116
|
+
"Rajasthan",
|
|
117
|
+
"Sikkim",
|
|
118
|
+
"Telangana",
|
|
119
|
+
"Tripura",
|
|
120
|
+
"Uttar Pradesh",
|
|
121
|
+
"Uttarakhand",
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
# GRU model configuration
|
|
125
|
+
GRU_HIDDEN_SIZE = 2048
|
|
126
|
+
GRU_ALL_LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.,;"
|
|
127
|
+
GRU_N_LETTERS = len(GRU_ALL_LETTERS)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
state,official_languages,addl_official_languages,most_spoken_lang,second_most_spoken_lang,third_most_spoken_lang,fourth_most_spoken_lang,fifth_most_spoken_lang
|
|
2
|
+
Andaman and Nicobar Islands,"Hindi, English",,Bengali,Hindi,Tamil,Telugu,Malayalam
|
|
3
|
+
Andhra Pradesh,Telugu,Urdu,Telugu,Urdu,Tamil,Chenchu,Kannada
|
|
4
|
+
Arunachal Pradesh,English,,Nishi,Adi,Apatani,Tagin,Hindi
|
|
5
|
+
Assam,Assamese,"Bengali, Bodo",Assamese,Bengali,Bodo,Hindi,Nepali
|
|
6
|
+
Bihar,Hindi,Urdu,Hindi (including Bihari languages),Maithili,Urdu,Bengali,Santali
|
|
7
|
+
Chandigarh,English,,Hindi,Punjabi,Urdu,Nepali,Bengali
|
|
8
|
+
Chhattisgarh,Hindi,Chhattisgarhi,Hindi,Odia,Bengali,Telugu,Marathi
|
|
9
|
+
Dadra and Nagar Haveli and Daman and Diu,"Hindi, English",Gujarati,Gujarati,Hindi,Marathi,Konkani,Bengali
|
|
10
|
+
Delhi,"Hindi, English","Urdu, Punjabi",Hindi,Punjabi,Urdu,Bengali,Maithili
|
|
11
|
+
Goa,Konkani,Marathi,Konkani,Marathi,Hindi,Kannada,Urdu
|
|
12
|
+
Gujarat,Gujarati,,Gujarati,Hindi,Marathi,Sindhi,Urdu
|
|
13
|
+
Haryana,Hindi,Punjabi,Hindi,Punjabi,Urdu,Bengali,Maithili
|
|
14
|
+
Himachal Pradesh,Hindi,Sanskrit,Hindi (including Pahari languages),Punjabi,Nepali,Kashmiri,Dogri
|
|
15
|
+
Jammu and Kashmir and Ladakh,"Kashmiri, Dogri, Hindi, Urdu, English",,Kashmiri,Dogri,Hindi,Punjabi,Urdu
|
|
16
|
+
Jharkhand,Hindi,"Angika, Bengali, Bhojpuri, Bhumij, Ho, Kharia, Khortha, Kurmali, Kurukh, Magahi, Maithili, Mundari, Nagpuri, Odia, Santali, Urdu",Hindi (including Bihari languages),Santali,Bengali,Urdu,Odia
|
|
17
|
+
Karnataka,Kannada,,Kannada,Telugu,Tamil,Marathi,Kodava
|
|
18
|
+
Kerala,Malayalam,English,Malayalam,Tamil,Tulu,Kannada,Konkani
|
|
19
|
+
Lakshadweep,"English, Hindi",,Malayalam,Dhivehi,Tamil,Hindi,Telugu
|
|
20
|
+
Madhya Pradesh,Hindi,,Hindi,Marathi,Urdu,Sindhi,Gujarati
|
|
21
|
+
Maharashtra,Marathi,,Marathi,Hindi,Urdu,Gujarati,Telugu
|
|
22
|
+
Manipur,Meitei,English,Meitei,Nepali,Hindi,Bengali,English
|
|
23
|
+
Meghalaya,English,Khasi,Khasi,Garo,Bengali,Nepali,Hindi
|
|
24
|
+
Mizoram,"English, Hindi, Mizo",,Mizo,English,Hindi,Meitei,Chakma
|
|
25
|
+
Nagaland,English,,Naga languages,English,Hindi,Assamese,Meitei
|
|
26
|
+
Odisha,Odia,,Odia,Hindi,Santali,Urdu,Telugu
|
|
27
|
+
Puducherry,"Tamil, English, French",,Tamil,Telugu,Malayalam,French,English
|
|
28
|
+
Punjab,Punjabi,,Punjabi,Hindi,Urdu,Bengali,English
|
|
29
|
+
Rajasthan,Hindi,English,Hindi (including Rajasthani languages),Punjabi,Gujarati,Sindhi,Urdu
|
|
30
|
+
Sikkim,"English, Nepali","Bhutia, Gurung, Lepcha, Limbu, Manggar, Mukhia, Newari, Rai, Sherpa, Tamang",Nepali,Hindi,Bengali,Urdu,Punjabi
|
|
31
|
+
Tamil Nadu,Tamil,English,Tamil,Telugu,Kannada,Urdu,Malayalam
|
|
32
|
+
Telangana,Telugu,Urdu,Telugu,Urdu,Gondi,Kannada,Marathi
|
|
33
|
+
Tripura,"Bengali, English, Kokborok",,Bengali,Kokborok,Chakma,English,Meitei
|
|
34
|
+
Uttar Pradesh,Hindi,Urdu,Hindi (including UP local dialects),Urdu,Punjabi,Nepali,Bengali
|
|
35
|
+
Uttarakhand,Hindi,Sanskrit,Hindi,Pahari and Kumauni,Urdu,Punjabi,Bengali
|
|
36
|
+
West Bengal,"Bengali, English",,Bengali,Hindi,Santali,Urdu,Nepali
|
instate/electoral.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Electoral rolls based name-to-state lookup.
|
|
3
|
+
|
|
4
|
+
Functions for looking up state distributions from 2017 Indian electoral rolls data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_state_distribution(
|
|
15
|
+
names: pd.DataFrame | list[str], name_column: str | None = None
|
|
16
|
+
) -> pd.DataFrame:
|
|
17
|
+
"""Get P(state|lastname) from 2017 Indian electoral rolls.
|
|
18
|
+
|
|
19
|
+
This returns the empirical distribution of a lastname across Indian states
|
|
20
|
+
based on the electoral rolls data. This is the Bayes optimal estimate
|
|
21
|
+
given the observed frequencies.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
names: DataFrame containing names or list of name strings.
|
|
25
|
+
Names are automatically cleaned (lowercase, stripped).
|
|
26
|
+
name_column: If names is a DataFrame, the column containing names.
|
|
27
|
+
If None and DataFrame has 'name' or 'lastname', uses that.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
DataFrame with original data plus 31 state probability columns.
|
|
31
|
+
State columns are named by state (e.g., 'delhi', 'punjab').
|
|
32
|
+
Values are proportions (0-1) representing P(state|lastname).
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
>>> names = ["dhingra", "sood", "gowda"]
|
|
36
|
+
>>> result = get_state_distribution(names)
|
|
37
|
+
>>> result[["name", "delhi", "punjab", "karnataka"]]
|
|
38
|
+
|
|
39
|
+
>>> df = pd.DataFrame({"lastname": ["dhingra", "sood"]})
|
|
40
|
+
>>> result = get_state_distribution(df, "lastname")
|
|
41
|
+
>>> result.columns[:5].tolist()
|
|
42
|
+
"""
|
|
43
|
+
from ._utils import clean_names_in_df, load_electoral_data, prepare_name_dataframe
|
|
44
|
+
|
|
45
|
+
# Convert to DataFrame if needed
|
|
46
|
+
df = prepare_name_dataframe(names, name_column)
|
|
47
|
+
|
|
48
|
+
# Clean names for matching
|
|
49
|
+
df = clean_names_in_df(df, df.columns[0])
|
|
50
|
+
|
|
51
|
+
# Load electoral rolls data
|
|
52
|
+
electoral_data = load_electoral_data()
|
|
53
|
+
|
|
54
|
+
# Merge to get state distributions
|
|
55
|
+
# Electoral data has __last_name as key
|
|
56
|
+
result = pd.merge(
|
|
57
|
+
df, electoral_data, left_on="__cleaned_name", right_on="__last_name", how="left"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Drop temporary columns
|
|
61
|
+
result = result.drop(columns=["__cleaned_name", "__last_name"], errors="ignore")
|
|
62
|
+
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_state_languages(
|
|
67
|
+
states: pd.DataFrame | list[str], state_column: str | None = None
|
|
68
|
+
) -> pd.DataFrame:
|
|
69
|
+
"""Map Indian states to their official languages.
|
|
70
|
+
|
|
71
|
+
Based on census data, returns the official language(s) for each state.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
states: DataFrame containing states or list of state names.
|
|
75
|
+
state_column: If states is a DataFrame, the column containing state names.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
DataFrame with state and official_languages columns.
|
|
79
|
+
If input was DataFrame, adds official_languages column.
|
|
80
|
+
|
|
81
|
+
Examples:
|
|
82
|
+
>>> states = ["Delhi", "Punjab", "Karnataka"]
|
|
83
|
+
>>> result = get_state_languages(states)
|
|
84
|
+
>>> result[["state", "official_languages"]]
|
|
85
|
+
|
|
86
|
+
>>> df = pd.DataFrame({"state_name": ["Delhi", "Punjab"]})
|
|
87
|
+
>>> result = get_state_languages(df, "state_name")
|
|
88
|
+
"""
|
|
89
|
+
# Prepare DataFrame
|
|
90
|
+
if isinstance(states, list):
|
|
91
|
+
df = pd.DataFrame({"state": states})
|
|
92
|
+
state_col = "state"
|
|
93
|
+
else:
|
|
94
|
+
df = states.copy()
|
|
95
|
+
if state_column is None:
|
|
96
|
+
# Try to find state column
|
|
97
|
+
possible_cols = [c for c in df.columns if "state" in c.lower()]
|
|
98
|
+
if not possible_cols:
|
|
99
|
+
raise ValueError("state_column must be specified for DataFrame input")
|
|
100
|
+
state_col = possible_cols[0]
|
|
101
|
+
else:
|
|
102
|
+
state_col = state_column
|
|
103
|
+
|
|
104
|
+
# Load state-language mapping
|
|
105
|
+
data_dir = os.path.dirname(__file__)
|
|
106
|
+
state_lang_path = os.path.join(data_dir, "data", "state_to_languages.csv")
|
|
107
|
+
state_lang_map = pd.read_csv(state_lang_path)
|
|
108
|
+
|
|
109
|
+
# Merge to add languages
|
|
110
|
+
result = df.merge(state_lang_map, left_on=state_col, right_on="state", how="left")
|
|
111
|
+
|
|
112
|
+
# Clean up duplicate state column if needed
|
|
113
|
+
if state_col != "state" and "state_y" not in result.columns:
|
|
114
|
+
result = result.drop(columns=["state"], errors="ignore")
|
|
115
|
+
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def list_available_states() -> list[str]:
|
|
120
|
+
"""List all states available in the electoral rolls dataset.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
List of state names available in the data.
|
|
124
|
+
|
|
125
|
+
Examples:
|
|
126
|
+
>>> states = list_available_states()
|
|
127
|
+
>>> len(states)
|
|
128
|
+
31
|
|
129
|
+
>>> "Delhi" in states
|
|
130
|
+
True
|
|
131
|
+
"""
|
|
132
|
+
from ._utils import load_electoral_data
|
|
133
|
+
|
|
134
|
+
electoral_data = load_electoral_data()
|
|
135
|
+
# Get column names, excluding non-state columns
|
|
136
|
+
state_cols = [
|
|
137
|
+
col
|
|
138
|
+
for col in electoral_data.columns
|
|
139
|
+
if not col.startswith("__") and col != "last_name"
|
|
140
|
+
]
|
|
141
|
+
return sorted(state_cols)
|
instate/nnets.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
|
|
4
|
+
from .constants import GRU_ALL_LETTERS, GRU_HIDDEN_SIZE, GRU_N_LETTERS
|
|
5
|
+
|
|
6
|
+
# For backward compatibility with existing code
|
|
7
|
+
n_hidden = GRU_HIDDEN_SIZE
|
|
8
|
+
all_letters = GRU_ALL_LETTERS
|
|
9
|
+
n_letters = GRU_N_LETTERS
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def infer(net, name: str):
|
|
13
|
+
net.eval()
|
|
14
|
+
name_ohe = name_rep(name)
|
|
15
|
+
hidden = net.init_hidden()
|
|
16
|
+
|
|
17
|
+
for i in range(name_ohe.size()[0]):
|
|
18
|
+
output, hidden = net(name_ohe[i], hidden)
|
|
19
|
+
|
|
20
|
+
return output
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def name_rep(name: str):
|
|
24
|
+
rep = torch.zeros(len(name), 1, n_letters)
|
|
25
|
+
for index, letter in enumerate(name):
|
|
26
|
+
pos = all_letters.find(letter)
|
|
27
|
+
rep[index][0][pos] = 1
|
|
28
|
+
return rep
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class GRU_net(nn.Module):
|
|
32
|
+
def __init__(self, input_size, hidden_size, output_size):
|
|
33
|
+
super().__init__()
|
|
34
|
+
self.hidden_size = hidden_size
|
|
35
|
+
self.gru_cell = nn.GRU(input_size, hidden_size)
|
|
36
|
+
self.h2o = nn.Linear(hidden_size, output_size)
|
|
37
|
+
self.softmax = nn.LogSoftmax(dim=2)
|
|
38
|
+
|
|
39
|
+
def forward(self, input_, hidden):
|
|
40
|
+
out, hidden = self.gru_cell(input_.view(1, 1, -1), hidden)
|
|
41
|
+
output = self.h2o(hidden)
|
|
42
|
+
output = self.softmax(output)
|
|
43
|
+
return output.view(1, -1), hidden
|
|
44
|
+
|
|
45
|
+
def init_hidden(self):
|
|
46
|
+
return torch.zeros(1, 1, self.hidden_size)
|
instate/predict.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Neural network predictions for names not in electoral rolls.
|
|
3
|
+
|
|
4
|
+
Functions for predicting states and languages using trained models.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from functools import partial
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
import torch
|
|
13
|
+
from Levenshtein import distance
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def predict_state(
|
|
17
|
+
names: pd.DataFrame | list[str],
|
|
18
|
+
name_column: str | None = None,
|
|
19
|
+
top_k: int = 3,
|
|
20
|
+
model: str = "gru",
|
|
21
|
+
) -> pd.DataFrame:
|
|
22
|
+
"""Predict most likely Indian states for given names using neural network.
|
|
23
|
+
|
|
24
|
+
Uses a trained GRU model to predict which Indian states a person with
|
|
25
|
+
the given lastname is most likely to be from. This is useful for names
|
|
26
|
+
not found in the electoral rolls data.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
names: DataFrame containing names or list of name strings.
|
|
30
|
+
Names are automatically cleaned (lowercase, stripped).
|
|
31
|
+
name_column: If names is a DataFrame, the column containing names.
|
|
32
|
+
top_k: Number of top states to return (default: 3).
|
|
33
|
+
model: Model to use for prediction. Currently only "gru" supported.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
DataFrame with name and predicted_states columns.
|
|
37
|
+
predicted_states contains a list of top_k state names.
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
>>> names = ["dhingra", "sood", "gowda"]
|
|
41
|
+
>>> result = predict_state(names, top_k=3)
|
|
42
|
+
>>> result["predicted_states"][0]
|
|
43
|
+
['Delhi', 'Punjab', 'Haryana']
|
|
44
|
+
|
|
45
|
+
>>> df = pd.DataFrame({"lastname": ["sharma", "patel"]})
|
|
46
|
+
>>> result = predict_state(df, "lastname", top_k=2)
|
|
47
|
+
>>> len(result["predicted_states"][0])
|
|
48
|
+
2
|
|
49
|
+
"""
|
|
50
|
+
from ._utils import clean_name, load_gru_model, prepare_name_dataframe
|
|
51
|
+
from .constants import GT_KEYS
|
|
52
|
+
from .nnets import infer
|
|
53
|
+
|
|
54
|
+
if model != "gru":
|
|
55
|
+
raise ValueError(f"Model '{model}' not supported. Use 'gru'.")
|
|
56
|
+
|
|
57
|
+
# Prepare DataFrame
|
|
58
|
+
df = prepare_name_dataframe(names, name_column)
|
|
59
|
+
name_col = df.columns[0]
|
|
60
|
+
|
|
61
|
+
# Load model
|
|
62
|
+
gru_model = load_gru_model()
|
|
63
|
+
|
|
64
|
+
# Predict for each name
|
|
65
|
+
predictions = []
|
|
66
|
+
for name in df[name_col]:
|
|
67
|
+
cleaned = clean_name(name)
|
|
68
|
+
if not cleaned or len(cleaned) < 3:
|
|
69
|
+
predictions.append([])
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
# Run inference
|
|
73
|
+
output = infer(gru_model, cleaned)
|
|
74
|
+
_, indices = output.topk(top_k)
|
|
75
|
+
idx_list = indices.numpy().flatten().tolist()
|
|
76
|
+
pred_states = [GT_KEYS[i] for i in idx_list]
|
|
77
|
+
predictions.append(pred_states)
|
|
78
|
+
|
|
79
|
+
# Add predictions to DataFrame
|
|
80
|
+
result = df.copy()
|
|
81
|
+
result["predicted_states"] = predictions
|
|
82
|
+
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def predict_language(
|
|
87
|
+
names: pd.DataFrame | list[str],
|
|
88
|
+
name_column: str | None = None,
|
|
89
|
+
top_k: int = 3,
|
|
90
|
+
model: str = "lstm",
|
|
91
|
+
) -> pd.DataFrame:
|
|
92
|
+
"""Predict most likely languages for given names.
|
|
93
|
+
|
|
94
|
+
Two methods available:
|
|
95
|
+
- "lstm": Neural network prediction using trained LSTM model
|
|
96
|
+
- "knn": K-nearest neighbor lookup in language database
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
names: DataFrame containing names or list of name strings.
|
|
100
|
+
name_column: If names is a DataFrame, the column containing names.
|
|
101
|
+
top_k: Number of top languages to return (default: 3).
|
|
102
|
+
Note: KNN method returns only the single best match.
|
|
103
|
+
model: Prediction method - "lstm" (neural) or "knn" (lookup).
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
DataFrame with name and predicted_languages columns.
|
|
107
|
+
For LSTM: predicted_languages contains list of top_k languages.
|
|
108
|
+
For KNN: predicted_languages contains single best language.
|
|
109
|
+
|
|
110
|
+
Examples:
|
|
111
|
+
>>> names = ["sood", "chintalapati"]
|
|
112
|
+
>>> result = predict_language(names, model="lstm")
|
|
113
|
+
>>> result["predicted_languages"][0]
|
|
114
|
+
['hindi', 'punjabi', 'urdu']
|
|
115
|
+
|
|
116
|
+
>>> result_knn = predict_language(names, model="knn")
|
|
117
|
+
>>> result_knn["predicted_languages"][0]
|
|
118
|
+
'hindi'
|
|
119
|
+
|
|
120
|
+
>>> df = pd.DataFrame({"name": ["patel", "sharma"]})
|
|
121
|
+
>>> result = predict_language(df, "name", model="lstm", top_k=2)
|
|
122
|
+
>>> len(result["predicted_languages"][0])
|
|
123
|
+
2
|
|
124
|
+
"""
|
|
125
|
+
from ._utils import prepare_name_dataframe
|
|
126
|
+
|
|
127
|
+
# Prepare DataFrame
|
|
128
|
+
df = prepare_name_dataframe(names, name_column)
|
|
129
|
+
name_col = df.columns[0]
|
|
130
|
+
|
|
131
|
+
if model == "lstm":
|
|
132
|
+
predictions = _predict_language_lstm(df[name_col], top_k)
|
|
133
|
+
elif model == "knn":
|
|
134
|
+
predictions = _predict_language_knn(df[name_col])
|
|
135
|
+
else:
|
|
136
|
+
raise ValueError(f"Model '{model}' not supported. Use 'lstm' or 'knn'.")
|
|
137
|
+
|
|
138
|
+
# Add predictions to DataFrame
|
|
139
|
+
result = df.copy()
|
|
140
|
+
result["predicted_languages"] = predictions
|
|
141
|
+
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _predict_language_lstm(names: pd.Series, top_k: int = 3) -> list:
|
|
146
|
+
"""
|
|
147
|
+
Internal function for LSTM language prediction.
|
|
148
|
+
"""
|
|
149
|
+
from ._utils import clean_name, load_lstm_model
|
|
150
|
+
|
|
151
|
+
model, lstm_data = load_lstm_model()
|
|
152
|
+
char2idx = lstm_data["char2idx"]
|
|
153
|
+
idx2lang = lstm_data["idx2lang"]
|
|
154
|
+
device = lstm_data["device"]
|
|
155
|
+
|
|
156
|
+
predictions = []
|
|
157
|
+
|
|
158
|
+
for name in names:
|
|
159
|
+
cleaned = clean_name(name)
|
|
160
|
+
if not cleaned or len(cleaned) < 3:
|
|
161
|
+
predictions.append([])
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
# Convert name to indices
|
|
165
|
+
try:
|
|
166
|
+
name_indices = [char2idx.get(char, 0) for char in cleaned]
|
|
167
|
+
except Exception:
|
|
168
|
+
predictions.append([])
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
# Prepare tensor
|
|
172
|
+
with torch.no_grad():
|
|
173
|
+
name_tensor = (
|
|
174
|
+
torch.tensor(name_indices, dtype=torch.long).unsqueeze(0).to(device)
|
|
175
|
+
)
|
|
176
|
+
lengths = torch.tensor([len(cleaned)], dtype=torch.long)
|
|
177
|
+
|
|
178
|
+
# Get predictions for top 3 language outputs
|
|
179
|
+
out1, out2, out3 = model(name_tensor, lengths)
|
|
180
|
+
|
|
181
|
+
# Get top predictions from each output
|
|
182
|
+
pred_first = torch.argmax(out1, dim=1)
|
|
183
|
+
pred_second = torch.argmax(out2, dim=1)
|
|
184
|
+
pred_third = torch.argmax(out3, dim=1)
|
|
185
|
+
|
|
186
|
+
# Ensure unique predictions
|
|
187
|
+
if pred_second == pred_first:
|
|
188
|
+
pred_second = torch.topk(out2, k=2, dim=1)[1][0][1]
|
|
189
|
+
if pred_third == pred_first or pred_third == pred_second:
|
|
190
|
+
pred_third = torch.topk(out3, k=3, dim=1)[1][0][2]
|
|
191
|
+
|
|
192
|
+
# Convert to language names
|
|
193
|
+
langs = [
|
|
194
|
+
idx2lang[pred_first.item()],
|
|
195
|
+
idx2lang[pred_second.item()],
|
|
196
|
+
idx2lang[pred_third.item()],
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
# Return only top_k languages
|
|
200
|
+
predictions.append(langs[:top_k])
|
|
201
|
+
|
|
202
|
+
return predictions
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _predict_language_knn(names: pd.Series) -> list:
|
|
206
|
+
"""
|
|
207
|
+
Internal function for KNN language lookup.
|
|
208
|
+
"""
|
|
209
|
+
from ._utils import clean_name, load_language_lookup_data
|
|
210
|
+
|
|
211
|
+
lang_data = load_language_lookup_data()
|
|
212
|
+
lang_cols = lang_data.columns[1:] # Skip lastname column
|
|
213
|
+
|
|
214
|
+
predictions = []
|
|
215
|
+
|
|
216
|
+
for name in names:
|
|
217
|
+
cleaned = clean_name(name)
|
|
218
|
+
if not cleaned or len(cleaned) < 3:
|
|
219
|
+
predictions.append("")
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
# Calculate edit distance to all names in database
|
|
223
|
+
# Use partial to avoid lambda scope issue
|
|
224
|
+
distances = lang_data["last_name"].apply(partial(distance, cleaned))
|
|
225
|
+
|
|
226
|
+
# Get top 3 nearest names
|
|
227
|
+
nearest_indices = distances.nsmallest(3).index
|
|
228
|
+
|
|
229
|
+
# Sum language scores for nearest names and get max
|
|
230
|
+
lang_scores = lang_data.loc[nearest_indices, lang_cols].sum()
|
|
231
|
+
best_lang = lang_scores.idxmax()
|
|
232
|
+
|
|
233
|
+
predictions.append(best_lang)
|
|
234
|
+
|
|
235
|
+
return predictions
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: instate
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Instate: predict the state of residence from last name
|
|
5
|
+
Keywords: predict,state,residence,last name
|
|
6
|
+
Author: Atul Dhingra, Gaurav Sood, Rajashekar Chintalapati
|
|
7
|
+
Author-email: Atul Dhingra <dhingra.atul92@gmail.com>, Gaurav Sood <gsood07@gmail.com>, Rajashekar Chintalapati <rajshekar.ch@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Requires-Dist: pandas>=1.5.0
|
|
19
|
+
Requires-Dist: numpy>=1.21.0,<2.0.0
|
|
20
|
+
Requires-Dist: torch>=2.0.0
|
|
21
|
+
Requires-Dist: levenshtein>=0.20.0
|
|
22
|
+
Requires-Dist: requests>=2.25.0
|
|
23
|
+
Requires-Dist: tqdm>=4.64.0
|
|
24
|
+
Requires-Dist: sphinx>=7.0 ; extra == 'docs'
|
|
25
|
+
Requires-Dist: furo>=2024.1.29 ; extra == 'docs'
|
|
26
|
+
Requires-Dist: sphinx-autodoc-typehints>=1.25 ; extra == 'docs'
|
|
27
|
+
Requires-Dist: sphinx-copybutton>=0.5 ; extra == 'docs'
|
|
28
|
+
Requires-Dist: myst-parser>=2.0 ; extra == 'docs'
|
|
29
|
+
Requires-Python: >=3.11, <3.14
|
|
30
|
+
Project-URL: Bug Tracker, https://github.com/appeler/instate/issues
|
|
31
|
+
Project-URL: Homepage, https://github.com/appeler/instate
|
|
32
|
+
Project-URL: Repository, https://github.com/appeler/instate
|
|
33
|
+
Provides-Extra: docs
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
## instate: predict spoken language and the state of residence from last name
|
|
37
|
+
|
|
38
|
+
[](https://github.com/appeler/instate/actions?query=workflow%3Atest)
|
|
39
|
+
[](https://pypi.python.org/pypi/instate)
|
|
40
|
+
[](https://github.com/appeler/instate/actions/workflows/docs.yml)
|
|
41
|
+
[](https://pepy.tech/project/instate)
|
|
42
|
+
|
|
43
|
+
Using the Indian electoral rolls data (2017), we provide a Python
|
|
44
|
+
package that takes the last name of a person and gives its distribution
|
|
45
|
+
across states. This package can also predict the spoken language of the
|
|
46
|
+
person based on the last name.
|
|
47
|
+
|
|
48
|
+
# Potential Use Cases
|
|
49
|
+
|
|
50
|
+
India has 22 official languages. To serve such a diverse language base
|
|
51
|
+
is a challenge for businesses and surveyors. To the extent that
|
|
52
|
+
businesses have access to the last name (and no other information) and
|
|
53
|
+
in the absence of other data that allows us to model a person\'s spoken
|
|
54
|
+
language, the distribution of last names across states is the best we
|
|
55
|
+
have.
|
|
56
|
+
|
|
57
|
+
# Dataset
|
|
58
|
+
|
|
59
|
+
Refer
|
|
60
|
+
[lastname_langs_india.csv.tar.gz](https://github.com/appeler/instate/blob/main/instate/data/lastname_langs_india.csv.tar.gz)
|
|
61
|
+
for the dataset, that will be used to predict/lookup the spoken language
|
|
62
|
+
based on the last name.
|
|
63
|
+
|
|
64
|
+
Refer
|
|
65
|
+
[lastname_langs_india_top3.csv.tar.gz](https://github.com/appeler/instate/blob/main/instate/data/lastname_langs_india_top3.csv.tar.gz)
|
|
66
|
+
for the dataset, that will be used to predict the top-3 spoken languages
|
|
67
|
+
based on the last name. A LSTM model has been trained on this dataset to
|
|
68
|
+
predict the top-3 spoken languages.
|
|
69
|
+
|
|
70
|
+
Refer
|
|
71
|
+
[notebooks](https://github.com/appeler/instate/tree/main/instate/notebooks)
|
|
72
|
+
for the notebooks that were used to prepare above datasets and train the
|
|
73
|
+
models.
|
|
74
|
+
|
|
75
|
+
# Web UI
|
|
76
|
+
|
|
77
|
+
Streamlit App.:
|
|
78
|
+
<https://appeler-instate-streamlitstreamlit-app-e39m4c.streamlit.app/>
|
|
79
|
+
|
|
80
|
+
# Installation
|
|
81
|
+
|
|
82
|
+
We strongly recommend installing [instate]{.title-ref} inside a Python
|
|
83
|
+
virtual environment (see [venv
|
|
84
|
+
documentation](https://docs.python.org/3/library/venv.html#creating-virtual-environments))
|
|
85
|
+
|
|
86
|
+
pip install instate
|
|
87
|
+
|
|
88
|
+
# Examples
|
|
89
|
+
|
|
90
|
+
from instate import last_state
|
|
91
|
+
last_dat <- pd.read_csv("last_dat.csv")
|
|
92
|
+
last_state_dat <- last_state(last_dat, "dhingra")
|
|
93
|
+
print(last_state_dat)
|
|
94
|
+
|
|
95
|
+
# API
|
|
96
|
+
|
|
97
|
+
instate provides 4 main functions for predicting state and language from Indian lastnames.
|
|
98
|
+
|
|
99
|
+
## Electoral Rolls Lookup
|
|
100
|
+
|
|
101
|
+
- **get_state_distribution** - Get P(state|lastname) from 2017 electoral rolls data
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
import instate
|
|
105
|
+
|
|
106
|
+
# With list of names
|
|
107
|
+
names = ["sharma", "patel", "singh"]
|
|
108
|
+
result = instate.get_state_distribution(names)
|
|
109
|
+
print(result[["name", "Delhi", "Gujarat", "Punjab"]].head())
|
|
110
|
+
|
|
111
|
+
# With DataFrame
|
|
112
|
+
import pandas as pd
|
|
113
|
+
df = pd.DataFrame({"lastname": ["sharma", "patel"]})
|
|
114
|
+
result = instate.get_state_distribution(df, "lastname")
|
|
115
|
+
print(result.shape) # (2, 33) - 2 names + 31 state columns
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
- **get_state_languages** - Map states to their official languages
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
# Map states to languages
|
|
122
|
+
states = ["Delhi", "Punjab", "Gujarat"]
|
|
123
|
+
result = instate.get_state_languages(states)
|
|
124
|
+
print(result[["state", "official_languages"]])
|
|
125
|
+
|
|
126
|
+
# state official_languages
|
|
127
|
+
# 0 Delhi Hindi, English
|
|
128
|
+
# 1 Punjab Punjabi
|
|
129
|
+
# 2 Gujarat Gujarati
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Neural Network Predictions
|
|
133
|
+
|
|
134
|
+
- **predict_state** - Predict likely states using trained GRU model
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
# Predict top 3 most likely states
|
|
138
|
+
names = ["sharma", "patel", "singh"]
|
|
139
|
+
result = instate.predict_state(names, top_k=3)
|
|
140
|
+
print(result["predicted_states"].iloc[0])
|
|
141
|
+
# ['Delhi', 'Uttar Pradesh', 'Bihar']
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
- **predict_language** - Predict likely languages using LSTM or k-nearest neighbor
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
# LSTM neural network prediction (top 3)
|
|
148
|
+
result = instate.predict_language(names, model="lstm", top_k=3)
|
|
149
|
+
print(result["predicted_languages"].iloc[0])
|
|
150
|
+
# ['hindi', 'punjabi', 'urdu']
|
|
151
|
+
|
|
152
|
+
# K-nearest neighbor lookup (single best)
|
|
153
|
+
result = instate.predict_language(names, model="knn")
|
|
154
|
+
print(result["predicted_languages"].iloc[0])
|
|
155
|
+
# 'hindi'
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Complete Example
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
import pandas as pd
|
|
162
|
+
import instate
|
|
163
|
+
|
|
164
|
+
# Sample data
|
|
165
|
+
df = pd.DataFrame({
|
|
166
|
+
"person_id": [1, 2, 3],
|
|
167
|
+
"lastname": ["sharma", "patel", "singh"]
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
# Get state distributions from electoral rolls
|
|
171
|
+
state_dist = instate.get_state_distribution(df, "lastname")
|
|
172
|
+
print("Electoral rolls data shape:", state_dist.shape)
|
|
173
|
+
|
|
174
|
+
# Predict states with neural network
|
|
175
|
+
predicted_states = instate.predict_state(df, "lastname", top_k=3)
|
|
176
|
+
print("Top 3 predicted states:", predicted_states["predicted_states"].iloc[0])
|
|
177
|
+
|
|
178
|
+
# Predict languages
|
|
179
|
+
predicted_langs = instate.predict_language(df, "lastname", model="lstm", top_k=3)
|
|
180
|
+
print("Top 3 predicted languages:", predicted_langs["predicted_languages"].iloc[0])
|
|
181
|
+
|
|
182
|
+
# Map states to languages
|
|
183
|
+
states_df = pd.DataFrame({"state": ["Delhi", "Gujarat", "Punjab"]})
|
|
184
|
+
lang_map = instate.get_state_languages(states_df)
|
|
185
|
+
print("State language mapping:")
|
|
186
|
+
print(lang_map[["state", "official_languages"]])
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
# Data
|
|
190
|
+
|
|
191
|
+
The underlying data for the package can be accessed at:
|
|
192
|
+
<https://doi.org/10.7910/DVN/ZXMVTJ>
|
|
193
|
+
|
|
194
|
+
# Evaluation
|
|
195
|
+
|
|
196
|
+
The model has a top-3 accuracy of 85.3% on [unseen
|
|
197
|
+
names](https://github.com/appeler/instate/blob/main/instate/models/model_dnn_gpu.ipynb).
|
|
198
|
+
The KNN model does quite well. See the details
|
|
199
|
+
[here](https://github.com/appeler/instate/blob/main/instate/models/KNN_cosine_distance_simple_avg_modal_state.ipynb)
|
|
200
|
+
The name-to-language lookup has an accuracy of 67.9%. The
|
|
201
|
+
name-to-language model prediction has an accuracy of 72.2%.
|
|
202
|
+
|
|
203
|
+
# Authors
|
|
204
|
+
|
|
205
|
+
Atul Dhingra, Gaurav Sood and Rajashekar Chintalapati
|
|
206
|
+
|
|
207
|
+
# Contributor Code of Conduct
|
|
208
|
+
|
|
209
|
+
The project welcomes contributions from everyone! In fact, it depends on
|
|
210
|
+
it. To maintain this welcoming atmosphere, and to collaborate in a fun
|
|
211
|
+
and productive way, we expect contributors to the project to abide by
|
|
212
|
+
the [Contributor Code of
|
|
213
|
+
Conduct](http://contributor-covenant.org/version/1/0/0/).
|
|
214
|
+
|
|
215
|
+
# License
|
|
216
|
+
|
|
217
|
+
The package is released under the [MIT
|
|
218
|
+
License](https://opensource.org/licenses/MIT).
|
|
219
|
+
|
|
220
|
+
## 🔗 Adjacent Repositories
|
|
221
|
+
|
|
222
|
+
- [appeler/ethnicolr2](https://github.com/appeler/ethnicolr2) — Ethnicolr implementation with new models in pytorch
|
|
223
|
+
- [appeler/naampy](https://github.com/appeler/naampy) — Infer Sociodemographic Characteristics from Names Using Indian Electoral Rolls
|
|
224
|
+
- [appeler/ethnicolr](https://github.com/appeler/ethnicolr) — Predict Race and Ethnicity Based on the Sequence of Characters in a Name
|
|
225
|
+
- [appeler/parsernaam](https://github.com/appeler/parsernaam) — AI name parsing. Predict first or last name using a DL model.
|
|
226
|
+
- [appeler/ethnicolor](https://github.com/appeler/ethnicolor) — Race and Ethnicity based on name using data from census, voter reg. files, etc.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
instate/__init__.py,sha256=sfIy0tU2asluoXBjWRgQEOOEl8u_iKaLhEs9F_wV3as,924
|
|
2
|
+
instate/_utils.py,sha256=ITEjqTcQIry_xxWEtOvXiGGxDI0Gvv9SqPfASCwfcEM,9692
|
|
3
|
+
instate/constants.py,sha256=v12Bpw64UWqEUiH0ZWVld-dW4PFs_ZXvubkMQn3qzC4,2201
|
|
4
|
+
instate/data/instate_unique_ln_state_prop_v1.csv.gz,sha256=IfmHWpnwdGi_o-rQVL45kVmndyV6Ex1eO8m6hbXJ_XY,11166988
|
|
5
|
+
instate/data/lastname_langs_india.csv.tar.gz,sha256=DkwB0H6fNsD-qLAvxPSrbUSaHJEMX-l2aaYdAkytr-U,7470521
|
|
6
|
+
instate/data/lastname_langs_india_top3.csv.tar.gz,sha256=AQYOTSiI0uKF2qIARDYQRwQnUzceD6uwrTMCq1JzU1s,7470581
|
|
7
|
+
instate/data/state_lang_labels.pt,sha256=l_HDwC4fx0ebQdSheN10_SJrwjaRn76xUUcKmFIrfPE,434872
|
|
8
|
+
instate/data/state_to_languages.csv,sha256=GkXL9qwstNIWOrGplSbZAnHrkD7T5Q9C9xZY095xcbk,2781
|
|
9
|
+
instate/electoral.py,sha256=M2UnJrQtKWSEEMiFjIRVvw1RcNoTheSudOt3NyGzmu8,4634
|
|
10
|
+
instate/nnets.py,sha256=rHvXpqryWtYoinTW3zN2MIn1I7KYcRcjpNVbGYzsXyI,1266
|
|
11
|
+
instate/predict.py,sha256=dXl7CrxvnmXKcqtlI6DIkCir642DS8InjO1bPHo9eNg,7465
|
|
12
|
+
instate-1.0.0.dist-info/WHEEL,sha256=z-mOpxbJHqy3cq6SvUThBZdaLGFZzdZPtgWLcP2NKjQ,79
|
|
13
|
+
instate-1.0.0.dist-info/METADATA,sha256=ighh53kIzWntBIvj73_-5FQrw2oyKZKhUMjIZEIjwIw,8369
|
|
14
|
+
instate-1.0.0.dist-info/RECORD,,
|