Spaces:
Sleeping
Sleeping
Commit ·
6743083
1
Parent(s): b16d67a
Added interactive renaming feature for new districts
Browse files- README.md +1 -0
- app.py +63 -3
- district_mapper.py +2 -1
- 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 |
-
|
|
|
|
| 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)
|