| """ |
| TB Vulnerability Hotspot Prediction - Synthetic Data Generation |
| ================================================================ |
| Generates realistic district-level Indian TB vulnerability data based on |
| published distributions from NFHS-5, Census 2011, WHO GTB, and NIKSHAY. |
| |
| Feature categories: |
| 1. Demographic (Census 2011 distributions) |
| 2. Nutritional/Health (NFHS-5 distributions) |
| 3. Socioeconomic (SECC/Census) |
| 4. Environmental (ERA5/CPCB distributions) |
| 5. Healthcare Infrastructure (HMIS/NFHS) |
| 6. Spatial (lat/lon of district centroids) |
| """ |
|
|
| import numpy as np |
| import pandas as pd |
| from scipy import stats |
|
|
| np.random.seed(42) |
|
|
| |
| |
| |
| |
|
|
| STATES_CONFIG = { |
| 'Andhra Pradesh': {'n_districts': 13, 'lat': (13.5, 18.5), 'lon': (77.0, 84.0), 'region': 'South'}, |
| 'Arunachal Pradesh': {'n_districts': 25, 'lat': (26.5, 29.0), 'lon': (91.5, 97.5), 'region': 'NorthEast'}, |
| 'Assam': {'n_districts': 33, 'lat': (24.0, 27.5), 'lon': (89.5, 96.0), 'region': 'NorthEast'}, |
| 'Bihar': {'n_districts': 38, 'lat': (24.0, 27.5), 'lon': (83.5, 88.0), 'region': 'East'}, |
| 'Chhattisgarh': {'n_districts': 27, 'lat': (18.5, 24.0), 'lon': (80.5, 84.0), 'region': 'Central'}, |
| 'Goa': {'n_districts': 2, 'lat': (15.0, 15.8), 'lon': (73.5, 74.3), 'region': 'West'}, |
| 'Gujarat': {'n_districts': 33, 'lat': (20.5, 24.5), 'lon': (68.5, 74.5), 'region': 'West'}, |
| 'Haryana': {'n_districts': 22, 'lat': (27.5, 30.5), 'lon': (74.5, 77.5), 'region': 'North'}, |
| 'Himachal Pradesh': {'n_districts': 12, 'lat': (30.5, 33.5), 'lon': (75.5, 79.0), 'region': 'North'}, |
| 'Jharkhand': {'n_districts': 24, 'lat': (22.0, 25.5), 'lon': (83.5, 87.5), 'region': 'East'}, |
| 'Karnataka': {'n_districts': 30, 'lat': (11.5, 18.5), 'lon': (74.0, 78.5), 'region': 'South'}, |
| 'Kerala': {'n_districts': 14, 'lat': (8.0, 12.5), 'lon': (75.0, 77.5), 'region': 'South'}, |
| 'Madhya Pradesh': {'n_districts': 52, 'lat': (21.0, 26.5), 'lon': (74.0, 82.5), 'region': 'Central'}, |
| 'Maharashtra': {'n_districts': 36, 'lat': (15.5, 22.0), 'lon': (72.5, 80.5), 'region': 'West'}, |
| 'Manipur': {'n_districts': 16, 'lat': (24.0, 26.0), 'lon': (93.0, 94.5), 'region': 'NorthEast'}, |
| 'Meghalaya': {'n_districts': 11, 'lat': (25.0, 26.0), 'lon': (89.5, 92.5), 'region': 'NorthEast'}, |
| 'Mizoram': {'n_districts': 8, 'lat': (21.5, 24.5), 'lon': (92.0, 93.5), 'region': 'NorthEast'}, |
| 'Nagaland': {'n_districts': 11, 'lat': (25.0, 27.0), 'lon': (93.5, 95.5), 'region': 'NorthEast'}, |
| 'Odisha': {'n_districts': 30, 'lat': (18.0, 22.5), 'lon': (81.5, 87.5), 'region': 'East'}, |
| 'Punjab': {'n_districts': 22, 'lat': (29.5, 32.5), 'lon': (73.5, 76.5), 'region': 'North'}, |
| 'Rajasthan': {'n_districts': 33, 'lat': (23.0, 30.0), 'lon': (69.0, 78.0), 'region': 'North'}, |
| 'Sikkim': {'n_districts': 4, 'lat': (27.0, 28.0), 'lon': (88.0, 89.0), 'region': 'NorthEast'}, |
| 'Tamil Nadu': {'n_districts': 38, 'lat': (8.0, 13.5), 'lon': (76.0, 80.5), 'region': 'South'}, |
| 'Telangana': {'n_districts': 33, 'lat': (15.5, 19.5), 'lon': (77.0, 81.0), 'region': 'South'}, |
| 'Tripura': {'n_districts': 8, 'lat': (22.5, 24.5), 'lon': (91.0, 92.5), 'region': 'NorthEast'}, |
| 'Uttar Pradesh': {'n_districts': 75, 'lat': (23.5, 30.5), 'lon': (77.0, 84.5), 'region': 'North'}, |
| 'Uttarakhand': {'n_districts': 13, 'lat': (28.5, 31.5), 'lon': (77.5, 81.0), 'region': 'North'}, |
| 'West Bengal': {'n_districts': 23, 'lat': (21.5, 27.5), 'lon': (86.5, 89.5), 'region': 'East'}, |
| 'Delhi': {'n_districts': 11, 'lat': (28.4, 28.9), 'lon': (76.8, 77.4), 'region': 'North'}, |
| 'Jammu & Kashmir': {'n_districts': 20, 'lat': (32.5, 37.0), 'lon': (73.5, 80.0), 'region': 'North'}, |
| 'Ladakh': {'n_districts': 2, 'lat': (32.5, 35.5), 'lon': (75.5, 78.5), 'region': 'North'}, |
| 'Puducherry': {'n_districts': 4, 'lat': (10.5, 12.0), 'lon': (79.5, 80.0), 'region': 'South'}, |
| 'Chandigarh': {'n_districts': 1, 'lat': (30.7, 30.8), 'lon': (76.7, 76.8), 'region': 'North'}, |
| 'Andaman & Nicobar': {'n_districts': 3, 'lat': (7.0, 13.5), 'lon': (92.0, 94.0), 'region': 'Islands'}, |
| 'Dadra & Nagar Haveli': {'n_districts': 1, 'lat': (20.1, 20.4), 'lon': (72.9, 73.3), 'region': 'West'}, |
| 'Lakshadweep': {'n_districts': 1, 'lat': (10.0, 12.5), 'lon': (71.5, 74.0), 'region': 'Islands'}, |
| } |
|
|
| def generate_district_data(): |
| """Generate realistic district-level multi-source data.""" |
| |
| records = [] |
| district_id = 0 |
| |
| for state, config in STATES_CONFIG.items(): |
| n = config['n_districts'] |
| region = config['region'] |
| |
| for i in range(n): |
| district_id += 1 |
| lat = np.random.uniform(*config['lat']) |
| lon = np.random.uniform(*config['lon']) |
| |
| |
| |
| |
| |
| population = int(np.random.lognormal(mean=13.5, sigma=1.2)) |
| population = np.clip(population, 10000, 12000000) |
| |
| |
| urban_base = {'North': 0.34, 'South': 0.38, 'East': 0.18, |
| 'West': 0.42, 'Central': 0.22, 'NorthEast': 0.15, 'Islands': 0.35} |
| urban_pct = np.clip(np.random.beta(2, 5) * 0.8 + urban_base.get(region, 0.3) * 0.4, 0.02, 0.99) |
| |
| |
| if 'Delhi' in state: |
| pop_density = np.random.uniform(5000, 30000) |
| elif urban_pct > 0.6: |
| pop_density = np.random.lognormal(7.0, 0.8) |
| else: |
| pop_density = np.random.lognormal(5.5, 1.0) |
| pop_density = np.clip(pop_density, 5, 35000) |
| |
| |
| literacy_base = {'South': 0.82, 'North': 0.72, 'East': 0.65, |
| 'West': 0.78, 'Central': 0.68, 'NorthEast': 0.73, 'Islands': 0.86} |
| literacy_rate = np.clip( |
| np.random.normal(literacy_base.get(region, 0.72), 0.08), 0.40, 0.98 |
| ) |
| |
| |
| sc_pct = np.clip(np.random.beta(2, 8), 0.0, 0.50) |
| st_pct = np.clip(np.random.beta(1.5, 10) if region != 'NorthEast' else np.random.beta(5, 3), 0.0, 0.95) |
| |
| |
| household_size = np.clip(np.random.normal(4.9, 0.8), 2.0, 9.0) |
| |
| |
| persons_per_room = np.clip(np.random.lognormal(0.5, 0.4), 1.0, 6.0) |
| |
| |
| migration_rate = np.clip(np.random.beta(2, 10), 0.01, 0.30) |
| |
| |
| |
| |
| |
| stunting_rate = np.clip( |
| np.random.normal(0.35 - 0.1 * (literacy_rate - 0.7), 0.08), 0.08, 0.60 |
| ) |
| |
| |
| anaemia_pct = np.clip( |
| np.random.normal(0.57 - 0.15 * (literacy_rate - 0.7), 0.10), 0.20, 0.85 |
| ) |
| |
| |
| underweight_pct = np.clip( |
| np.random.normal(0.187 + 0.2 * (1 - urban_pct), 0.06), 0.02, 0.45 |
| ) |
| |
| |
| diabetes_pct = np.clip( |
| np.random.normal(0.10 + 0.05 * urban_pct, 0.04), 0.02, 0.25 |
| ) |
| |
| |
| hiv_high_states = ['Maharashtra', 'Karnataka', 'Andhra Pradesh', 'Telangana', |
| 'Tamil Nadu', 'Manipur', 'Nagaland', 'Mizoram'] |
| hiv_base = 0.005 if state in hiv_high_states else 0.001 |
| hiv_prevalence = np.clip(np.random.exponential(hiv_base), 0.0001, 0.03) |
| |
| |
| bcg_coverage = np.clip(np.random.normal(0.92, 0.06), 0.50, 0.99) |
| |
| |
| |
| |
| |
| bpl_pct = np.clip( |
| np.random.normal(0.22 + 0.15 * (1 - literacy_rate), 0.08), 0.01, 0.65 |
| ) |
| |
| |
| female_literacy_gap = np.clip(np.random.normal(0.17, 0.06), 0.0, 0.35) |
| |
| |
| worker_participation = np.clip(np.random.normal(0.40, 0.08), 0.15, 0.65) |
| |
| |
| open_defecation = np.clip( |
| np.random.beta(2, 5) * (1 - urban_pct) * 0.8, 0.0, 0.80 |
| ) |
| |
| |
| improved_water = np.clip( |
| np.random.normal(0.85 + 0.1 * urban_pct, 0.08), 0.40, 0.99 |
| ) |
| |
| |
| solid_fuel_pct = np.clip( |
| np.random.normal(0.55 - 0.3 * urban_pct, 0.12), 0.05, 0.90 |
| ) |
| |
| |
| |
| |
| |
| temp_mean = np.clip(35 - 0.5 * (lat - 8) + np.random.normal(0, 2), 15, 35) |
| |
| |
| humidity = np.clip( |
| np.random.normal(65 + 10 * (1 / (1 + np.exp(-(lon - 80)))), 10), 30, 95 |
| ) |
| |
| |
| rainfall_base = {'NorthEast': 2500, 'South': 1200, 'West': 800, |
| 'North': 700, 'Central': 900, 'East': 1400, 'Islands': 3000} |
| annual_rainfall = np.clip( |
| np.random.lognormal(np.log(rainfall_base.get(region, 1000)), 0.4), 200, 6000 |
| ) |
| |
| |
| pm25_base = {'North': 90, 'East': 75, 'Central': 60, 'West': 45, |
| 'South': 35, 'NorthEast': 30, 'Islands': 20} |
| pm25 = np.clip( |
| np.random.lognormal(np.log(pm25_base.get(region, 50)), 0.4), 5, 200 |
| ) |
| |
| |
| if region == 'North' and lat > 30: |
| altitude = np.random.lognormal(7.5, 0.5) |
| elif region == 'NorthEast': |
| altitude = np.random.lognormal(6.0, 0.8) |
| else: |
| altitude = np.random.lognormal(5.0, 1.0) |
| altitude = np.clip(altitude, 0, 6000) |
| |
| |
| |
| |
| |
| phc_per_100k = np.clip(np.random.lognormal(1.5, 0.5), 0.5, 30) |
| |
| |
| beds_per_1000 = np.clip( |
| np.random.lognormal(-0.3 + 0.5 * urban_pct, 0.5), 0.1, 10.0 |
| ) |
| |
| |
| dmc_per_100k = np.clip(np.random.lognormal(0.5, 0.6), 0.1, 15.0) |
| |
| |
| treatment_success = np.clip( |
| np.random.normal(0.82 + 0.1 * (beds_per_1000 / 5), 0.08), 0.50, 0.98 |
| ) |
| |
| |
| institutional_delivery = np.clip( |
| np.random.normal(0.75 + 0.15 * urban_pct, 0.12), 0.20, 0.99 |
| ) |
| |
| |
| anc_4plus = np.clip( |
| np.random.normal(0.58 + 0.2 * urban_pct, 0.12), 0.15, 0.95 |
| ) |
| |
| |
| |
| |
| |
| |
| tb_risk_score = ( |
| 0.20 * (bpl_pct / 0.65) + |
| 0.15 * (persons_per_room / 6.0) + |
| 0.12 * (underweight_pct / 0.45) + |
| 0.12 * (stunting_rate / 0.60) + |
| 0.10 * (hiv_prevalence / 0.03) + |
| 0.08 * (pm25 / 200) + |
| 0.07 * (solid_fuel_pct / 0.90) + |
| 0.06 * (open_defecation / 0.80) + |
| 0.05 * (1 - treatment_success) + |
| 0.05 * (1 - literacy_rate) |
| ) |
| |
| |
| spatial_noise = np.random.normal(0, 0.05) |
| tb_risk_score = np.clip(tb_risk_score + spatial_noise, 0.05, 0.95) |
| |
| |
| tb_notification_rate = np.clip( |
| 50 + tb_risk_score * 550 + np.random.normal(0, 30), 20, 700 |
| ) |
| |
| |
| case_detection_rate = np.clip( |
| np.random.normal(0.75 + 0.15 * (dmc_per_100k / 10), 0.08), 0.40, 0.99 |
| ) |
| |
| |
| mdr_tb_pct = np.clip(np.random.exponential(0.028), 0.005, 0.15) |
| |
| |
| |
| |
| records.append({ |
| 'district_id': f'D{district_id:04d}', |
| 'state': state, |
| 'region': region, |
| 'latitude': round(lat, 4), |
| 'longitude': round(lon, 4), |
| |
| 'population': population, |
| 'urban_pct': round(urban_pct, 4), |
| 'pop_density': round(pop_density, 1), |
| 'literacy_rate': round(literacy_rate, 4), |
| 'sc_pct': round(sc_pct, 4), |
| 'st_pct': round(st_pct, 4), |
| 'household_size': round(household_size, 2), |
| 'persons_per_room': round(persons_per_room, 2), |
| 'migration_rate': round(migration_rate, 4), |
| |
| 'stunting_rate': round(stunting_rate, 4), |
| 'anaemia_pct': round(anaemia_pct, 4), |
| 'underweight_pct': round(underweight_pct, 4), |
| 'diabetes_pct': round(diabetes_pct, 4), |
| 'hiv_prevalence': round(hiv_prevalence, 6), |
| 'bcg_coverage': round(bcg_coverage, 4), |
| |
| 'bpl_pct': round(bpl_pct, 4), |
| 'female_literacy_gap': round(female_literacy_gap, 4), |
| 'worker_participation': round(worker_participation, 4), |
| 'open_defecation': round(open_defecation, 4), |
| 'improved_water': round(improved_water, 4), |
| 'solid_fuel_pct': round(solid_fuel_pct, 4), |
| |
| 'temp_mean': round(temp_mean, 1), |
| 'humidity': round(humidity, 1), |
| 'annual_rainfall': round(annual_rainfall, 0), |
| 'pm25': round(pm25, 1), |
| 'altitude': round(altitude, 0), |
| |
| 'phc_per_100k': round(phc_per_100k, 2), |
| 'beds_per_1000': round(beds_per_1000, 3), |
| 'dmc_per_100k': round(dmc_per_100k, 2), |
| 'treatment_success': round(treatment_success, 4), |
| 'institutional_delivery': round(institutional_delivery, 4), |
| 'anc_4plus': round(anc_4plus, 4), |
| |
| 'tb_notification_rate': round(tb_notification_rate, 1), |
| 'case_detection_rate': round(case_detection_rate, 4), |
| 'mdr_tb_pct': round(mdr_tb_pct, 4), |
| 'tb_risk_score': round(tb_risk_score, 4), |
| }) |
| |
| df = pd.DataFrame(records) |
| |
| |
| _add_spatial_autocorrelation(df) |
| |
| |
| _compute_hotspot_labels(df) |
| |
| |
| df['vulnerability_index'] = ( |
| 0.3 * df['tb_risk_score_spatial'] + |
| 0.2 * (df['tb_notification_rate'] / df['tb_notification_rate'].max()) + |
| 0.15 * (df['bpl_pct'] / df['bpl_pct'].max()) + |
| 0.1 * (df['persons_per_room'] / df['persons_per_room'].max()) + |
| 0.1 * (1 - df['treatment_success']) + |
| 0.1 * (df['underweight_pct'] / df['underweight_pct'].max()) + |
| 0.05 * (df['pm25'] / df['pm25'].max()) |
| ) |
| df['vulnerability_index'] = (df['vulnerability_index'] - df['vulnerability_index'].min()) / \ |
| (df['vulnerability_index'].max() - df['vulnerability_index'].min()) |
| |
| return df |
|
|
|
|
| def _add_spatial_autocorrelation(df): |
| """Add spatial autocorrelation - nearby districts should have correlated TB rates.""" |
| from scipy.spatial.distance import cdist |
| |
| coords = df[['latitude', 'longitude']].values |
| dist_matrix = cdist(coords, coords, metric='euclidean') |
| |
| |
| W = np.exp(-dist_matrix**2 / (2 * 2.0**2)) |
| np.fill_diagonal(W, 0) |
| W = W / W.sum(axis=1, keepdims=True) |
| |
| |
| tb_rates = df['tb_notification_rate'].values |
| spatial_smoothed = W @ tb_rates |
| |
| |
| alpha = 0.3 |
| df['tb_notification_rate'] = np.clip( |
| (1 - alpha) * tb_rates + alpha * spatial_smoothed + np.random.normal(0, 10, len(df)), |
| 20, 700 |
| ) |
| df['tb_risk_score_spatial'] = (df['tb_notification_rate'] - df['tb_notification_rate'].min()) / \ |
| (df['tb_notification_rate'].max() - df['tb_notification_rate'].min()) |
|
|
|
|
| def _compute_hotspot_labels(df): |
| """Compute hotspot labels using a Getis-Ord Gi*-like statistic.""" |
| from scipy.spatial.distance import cdist |
| |
| coords = df[['latitude', 'longitude']].values |
| dist_matrix = cdist(coords, coords, metric='euclidean') |
| |
| |
| d_threshold = 3.0 |
| W = (dist_matrix < d_threshold).astype(float) |
| np.fill_diagonal(W, 0) |
| |
| tb_rates = df['tb_notification_rate'].values |
| n = len(tb_rates) |
| x_bar = tb_rates.mean() |
| s = tb_rates.std() |
| |
| |
| gi_star = np.zeros(n) |
| for i in range(n): |
| wi = W[i] |
| wi_sum = wi.sum() |
| numerator = (wi * tb_rates).sum() - x_bar * wi_sum |
| denominator = s * np.sqrt((n * (wi**2).sum() - wi_sum**2) / (n - 1)) |
| gi_star[i] = numerator / denominator if denominator > 0 else 0 |
| |
| df['gi_star_zscore'] = gi_star |
| |
| |
| df['hotspot_label'] = 0 |
| df.loc[df['gi_star_zscore'] > 1.96, 'hotspot_label'] = 1 |
| df.loc[df['gi_star_zscore'] < -1.96, 'hotspot_label'] = -1 |
| |
| |
| df['is_hotspot'] = (df['gi_star_zscore'] > 1.65).astype(int) |
| |
| |
| df['vulnerability_class'] = pd.qcut( |
| df['tb_notification_rate'], q=4, labels=['Low', 'Moderate', 'High', 'Very High'] |
| ) |
|
|
|
|
| if __name__ == '__main__': |
| df = generate_district_data() |
| print(f"Generated {len(df)} districts across {df['state'].nunique()} states") |
| print(f"\nShape: {df.shape}") |
| print(f"\nColumns ({len(df.columns)}):") |
| for col in df.columns: |
| print(f" {col}: {df[col].dtype} | range: [{df[col].min():.4f}, {df[col].max():.4f}]" |
| if df[col].dtype in ['float64', 'int64'] else f" {col}: {df[col].dtype}") |
| |
| print(f"\nHotspot distribution:") |
| print(df['is_hotspot'].value_counts()) |
| print(f"\nVulnerability class distribution:") |
| print(df['vulnerability_class'].value_counts()) |
| print(f"\nTB notification rate stats:") |
| print(df['tb_notification_rate'].describe()) |
| |
| df.to_csv('/app/tb_vulnerability_pipeline/district_data.csv', index=False) |
| print(f"\nSaved to /app/tb_vulnerability_pipeline/district_data.csv") |
|
|