sharanyaswarup commited on
Commit
6743083
·
1 Parent(s): b16d67a

Added interactive renaming feature for new districts

Browse files
Files changed (4) hide show
  1. README.md +1 -0
  2. app.py +63 -3
  3. district_mapper.py +2 -1
  4. national_analysis.py +16 -0
README.md CHANGED
@@ -119,6 +119,7 @@ Applies all district corrections to the combined master sheet in one single oper
119
  1. Upload the **New Master Sheet** (downloaded from Tab 2).
120
  2. Upload the **Analysis File** (downloaded from Tab 3, optionally manually edited).
121
  3. Click **🔁 Apply District Back-Mapping**.
 
122
  - Runs UDISE Geo-Decoding for KVS/NVS/NAVY schools.
123
  - Applies the strict 3-tier district back-mapping.
124
  - Filters out schools outside the 21 Target States.
 
119
  1. Upload the **New Master Sheet** (downloaded from Tab 2).
120
  2. Upload the **Analysis File** (downloaded from Tab 3, optionally manually edited).
121
  3. Click **🔁 Apply District Back-Mapping**.
122
+ - **🆕 Rename New Districts (Optional):** If the analysis detects completely new districts, an interactive table will appear. You can click directly into the table and type the historical name you want to map them to. Your manual rules will take priority!
123
  - Runs UDISE Geo-Decoding for KVS/NVS/NAVY schools.
124
  - Applies the strict 3-tier district back-mapping.
125
  - Filters out schools outside the 21 Target States.
app.py CHANGED
@@ -615,6 +615,19 @@ def run_analysis(old_path, new_path):
615
  for ssddbb, name in geo_lookup["block"].items(): rows.append(["block", ssddbb, name])
616
  geo_df = pd.DataFrame(rows, columns=["Type", "Code", "Name"])
617
 
 
 
 
 
 
 
 
 
 
 
 
 
 
618
  return {
619
  "National Summary": ns,
620
  "State by State Breakdown": df_state_summary,
@@ -624,6 +637,7 @@ def run_analysis(old_path, new_path):
624
  "Block to District": block_to_district,
625
  "Block Mapping": block_mapping,
626
  "Category Transitions": cat_migrations,
 
627
  "UDISE Geo": geo_df,
628
  }
629
 
@@ -1117,7 +1131,7 @@ with gr.Blocks(title="School Data Fetcher", css=css, theme=custom_theme) as app:
1117
  sheet_order = [
1118
  "National Summary", "State by State Breakdown",
1119
  "District Mapping", "Complex Splits", "Block Mapping",
1120
- "Category Transitions", "All Added Schools", "All Deleted Schools", "UDISE Geo"
1121
  ]
1122
  with pd.ExcelWriter(tmp.name, engine="openpyxl") as writer:
1123
  for sheet in sheet_order:
@@ -1306,6 +1320,36 @@ with gr.Blocks(title="School Data Fetcher", css=css, theme=custom_theme) as app:
1306
  file_types=[".xlsx", ".csv", ".xls"],
1307
  )
1308
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1309
  with gr.Row():
1310
  remap_btn = gr.Button("🔁 Apply District Back-Mapping", variant="primary", scale=1)
1311
 
@@ -1353,7 +1397,7 @@ with gr.Blocks(title="School Data Fetcher", css=css, theme=custom_theme) as app:
1353
  # Internal state: stores the mapped DataFrame path
1354
  remap_df_path_state = gr.State(None)
1355
 
1356
- def ui_bulk_remap(master_file, analysis_file):
1357
  if master_file is None:
1358
  return (
1359
  "<p style='color:#ef4444;font-weight:600;'>⚠️ Please upload your new master sheet.</p>",
@@ -1379,6 +1423,22 @@ with gr.Blocks(title="School Data Fetcher", css=css, theme=custom_theme) as app:
1379
  xl = pd.ExcelFile(analysis_file.name)
1380
  an = {s: xl.parse(s) for s in xl.sheet_names}
1381
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1382
  if "UDISE Geo" in an:
1383
  geo_df = an["UDISE Geo"]
1384
  geo_lookup = {"state": {}, "district": {}, "block": {}}
@@ -1468,7 +1528,7 @@ with gr.Blocks(title="School Data Fetcher", css=css, theme=custom_theme) as app:
1468
 
1469
  remap_btn.click(
1470
  fn=ui_bulk_remap,
1471
- inputs=[remap_master_upload, remap_analysis_upload],
1472
  outputs=[remap_status_html, remap_report_df, remap_dl_btn, remap_df_path_state],
1473
  ).then(
1474
  fn=lambda p: gr.update(visible=p is not None),
 
615
  for ssddbb, name in geo_lookup["block"].items(): rows.append(["block", ssddbb, name])
616
  geo_df = pd.DataFrame(rows, columns=["Type", "Code", "Name"])
617
 
618
+ # Find unmapped completely new districts
619
+ old_dist_names = set(df_old["School_District__c"].dropna().str.strip().str.upper().unique())
620
+ new_dist_df = df_new[["School_State__c", "School_District__c"]].drop_duplicates().dropna()
621
+ new_dist_df["State"] = _clean_text(new_dist_df["School_State__c"])
622
+ new_dist_df["New District Name"] = _clean_text(new_dist_df["School_District__c"])
623
+
624
+ unmapped_new = new_dist_df[~new_dist_df["New District Name"].isin(old_dist_names)].copy()
625
+ unmapped_new["Rename To (Type here...)"] = ""
626
+ if not unmapped_new.empty:
627
+ unmapped_new = unmapped_new[["State", "New District Name", "Rename To (Type here...)"]].sort_values(["State", "New District Name"]).reset_index(drop=True)
628
+ else:
629
+ unmapped_new = pd.DataFrame(columns=["State", "New District Name", "Rename To (Type here...)"])
630
+
631
  return {
632
  "National Summary": ns,
633
  "State by State Breakdown": df_state_summary,
 
637
  "Block to District": block_to_district,
638
  "Block Mapping": block_mapping,
639
  "Category Transitions": cat_migrations,
640
+ "New Districts": unmapped_new,
641
  "UDISE Geo": geo_df,
642
  }
643
 
 
1131
  sheet_order = [
1132
  "National Summary", "State by State Breakdown",
1133
  "District Mapping", "Complex Splits", "Block Mapping",
1134
+ "Category Transitions", "All Added Schools", "All Deleted Schools", "New Districts", "UDISE Geo"
1135
  ]
1136
  with pd.ExcelWriter(tmp.name, engine="openpyxl") as writer:
1137
  for sheet in sheet_order:
 
1320
  file_types=[".xlsx", ".csv", ".xls"],
1321
  )
1322
 
1323
+ with gr.Accordion("🆕 Rename New Districts (Optional)", open=False, visible=False) as remap_new_districts_accordion:
1324
+ gr.HTML("<div style='font-size:0.85em; color:#6b7280; margin-bottom:10px;'>These are completely new districts found in the data. If you want to rename any of them, just click the <b>Rename To (Type here...)</b> cell and type the new name before clicking Apply.</div>")
1325
+ remap_new_districts_table = gr.Dataframe(
1326
+ headers=["State", "New District Name", "Rename To (Type here...)"],
1327
+ interactive=True,
1328
+ wrap=True,
1329
+ type="pandas"
1330
+ )
1331
+
1332
+ def on_analysis_upload(file):
1333
+ if file is None:
1334
+ return gr.update(visible=False), pd.DataFrame()
1335
+ try:
1336
+ if file.name.lower().endswith(".csv"):
1337
+ return gr.update(visible=False), pd.DataFrame()
1338
+ xl = pd.ExcelFile(file.name)
1339
+ if "New Districts" in xl.sheet_names:
1340
+ df = xl.parse("New Districts")
1341
+ if not df.empty:
1342
+ return gr.update(visible=True, open=True), df
1343
+ except Exception:
1344
+ pass
1345
+ return gr.update(visible=False), pd.DataFrame()
1346
+
1347
+ remap_analysis_upload.change(
1348
+ fn=on_analysis_upload,
1349
+ inputs=[remap_analysis_upload],
1350
+ outputs=[remap_new_districts_accordion, remap_new_districts_table]
1351
+ )
1352
+
1353
  with gr.Row():
1354
  remap_btn = gr.Button("🔁 Apply District Back-Mapping", variant="primary", scale=1)
1355
 
 
1397
  # Internal state: stores the mapped DataFrame path
1398
  remap_df_path_state = gr.State(None)
1399
 
1400
+ def ui_bulk_remap(master_file, analysis_file, new_districts_df):
1401
  if master_file is None:
1402
  return (
1403
  "<p style='color:#ef4444;font-weight:600;'>⚠️ Please upload your new master sheet.</p>",
 
1423
  xl = pd.ExcelFile(analysis_file.name)
1424
  an = {s: xl.parse(s) for s in xl.sheet_names}
1425
 
1426
+ # Merge the user's manual renames!
1427
+ if new_districts_df is not None and not new_districts_df.empty:
1428
+ # Extract the ones where they typed something
1429
+ new_districts_df["Rename To (Type here...)"] = new_districts_df["Rename To (Type here...)"].fillna("").astype(str)
1430
+ renames = new_districts_df[new_districts_df["Rename To (Type here...)"].str.strip() != ""]
1431
+ if not renames.empty:
1432
+ manual_dm = pd.DataFrame({
1433
+ "State": renames["State"].astype(str).str.strip().str.upper(),
1434
+ "Dist_OLD": renames["Rename To (Type here...)"].astype(str).str.strip().str.upper(),
1435
+ "Dist_NEW": renames["New District Name"].astype(str).str.strip().str.upper()
1436
+ })
1437
+ if "District Mapping" not in an:
1438
+ an["District Mapping"] = manual_dm
1439
+ else:
1440
+ an["District Mapping"] = pd.concat([manual_dm, an["District Mapping"]], ignore_index=True)
1441
+
1442
  if "UDISE Geo" in an:
1443
  geo_df = an["UDISE Geo"]
1444
  geo_lookup = {"state": {}, "district": {}, "block": {}}
 
1528
 
1529
  remap_btn.click(
1530
  fn=ui_bulk_remap,
1531
+ inputs=[remap_master_upload, remap_analysis_upload, remap_new_districts_table],
1532
  outputs=[remap_status_html, remap_report_df, remap_dl_btn, remap_df_path_state],
1533
  ).then(
1534
  fn=lambda p: gr.update(visible=p is not None),
district_mapper.py CHANGED
@@ -109,7 +109,8 @@ def build_mapper(analysis_dict: dict) -> dict:
109
  if key not in simple_map:
110
  simple_map[key] = old_dist
111
  # Track dominant (most schools) for ambiguous cases
112
- count = int(row.get("Affected_Schools", 0))
 
113
  if key not in dominant_map or count > dominant_map[key][1]:
114
  dominant_map[key] = (old_dist, count)
115
 
 
109
  if key not in simple_map:
110
  simple_map[key] = old_dist
111
  # Track dominant (most schools) for ambiguous cases
112
+ val = row.get("Affected_Schools", 0)
113
+ count = int(val) if pd.notna(val) else 0
114
  if key not in dominant_map or count > dominant_map[key][1]:
115
  dominant_map[key] = (old_dist, count)
116
 
national_analysis.py CHANGED
@@ -162,6 +162,19 @@ for ssdd, name in geo_lookup["district"].items(): rows.append(["district", ssdd,
162
  for ssddbb, name in geo_lookup["block"].items(): rows.append(["block", ssddbb, name])
163
  geo_df = pd.DataFrame(rows, columns=["Type", "Code", "Name"])
164
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  df_national_summary = pd.DataFrame(national_summary_data)
166
 
167
  export_file = os.path.join(folder_path, "national_analysis_summary.xlsx")
@@ -174,6 +187,9 @@ with pd.ExcelWriter(export_file, engine='openpyxl') as writer:
174
  df_added_raw.to_excel(writer, sheet_name='All Added Schools', index=False)
175
  if not df_deleted_raw.empty:
176
  df_deleted_raw.to_excel(writer, sheet_name='All Deleted Schools', index=False)
 
 
 
177
 
178
  if not district_mapping.empty:
179
  district_mapping.to_excel(writer, sheet_name='District Mapping', index=False)
 
162
  for ssddbb, name in geo_lookup["block"].items(): rows.append(["block", ssddbb, name])
163
  geo_df = pd.DataFrame(rows, columns=["Type", "Code", "Name"])
164
 
165
+ # Find unmapped completely new districts
166
+ old_dist_names = set(df_old["School_District__c"].dropna().astype(str).str.strip().str.upper().unique())
167
+ new_dist_df = df_new[["School_State__c", "School_District__c"]].drop_duplicates().dropna()
168
+ new_dist_df["State"] = new_dist_df["School_State__c"].astype(str).str.strip().str.upper()
169
+ new_dist_df["New District Name"] = new_dist_df["School_District__c"].astype(str).str.strip().str.upper()
170
+
171
+ unmapped_new = new_dist_df[~new_dist_df["New District Name"].isin(old_dist_names)].copy()
172
+ unmapped_new["Rename To (Type here...)"] = ""
173
+ if not unmapped_new.empty:
174
+ unmapped_new = unmapped_new[["State", "New District Name", "Rename To (Type here...)"]].sort_values(["State", "New District Name"]).reset_index(drop=True)
175
+ else:
176
+ unmapped_new = pd.DataFrame(columns=["State", "New District Name", "Rename To (Type here...)"])
177
+
178
  df_national_summary = pd.DataFrame(national_summary_data)
179
 
180
  export_file = os.path.join(folder_path, "national_analysis_summary.xlsx")
 
187
  df_added_raw.to_excel(writer, sheet_name='All Added Schools', index=False)
188
  if not df_deleted_raw.empty:
189
  df_deleted_raw.to_excel(writer, sheet_name='All Deleted Schools', index=False)
190
+
191
+ if not unmapped_new.empty:
192
+ unmapped_new.to_excel(writer, sheet_name='New Districts', index=False)
193
 
194
  if not district_mapping.empty:
195
  district_mapping.to_excel(writer, sheet_name='District Mapping', index=False)