Spaces:
Sleeping
Sleeping
Commit ·
6f32bed
1
Parent(s): dea1497
feat: added multi-year Master Analytics tab & updated documentation
Browse files- README.md +12 -1
- USER_MANUAL.md +24 -2
- app.py +185 -0
- master_analysis.py +71 -0
README.md
CHANGED
|
@@ -61,7 +61,7 @@ The absolute easiest way to use the pipeline is via the interactive browser UI.
|
|
| 61 |
```powershell
|
| 62 |
python app.py
|
| 63 |
```
|
| 64 |
-
The app opens automatically at `http://127.0.0.1:7861` and features **
|
| 65 |
|
| 66 |
---
|
| 67 |
|
|
@@ -112,6 +112,17 @@ A full database management dashboard to maintain your Scholarship Application ma
|
|
| 112 |
|
| 113 |
---
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
## 🛠️ Admin Scripts
|
| 116 |
|
| 117 |
If you need to make global, architectural changes to the baseline data outside of the UI, use the provided admin scripts:
|
|
|
|
| 61 |
```powershell
|
| 62 |
python app.py
|
| 63 |
```
|
| 64 |
+
The app opens automatically at `http://127.0.0.1:7861` and features **5 main tabs**.
|
| 65 |
|
| 66 |
---
|
| 67 |
|
|
|
|
| 112 |
|
| 113 |
---
|
| 114 |
|
| 115 |
+
### 📊 Tab 5: Master Analytics
|
| 116 |
+
|
| 117 |
+
A multi-year analytics dashboard powered strictly by Pandas to track macro-level trends across your historical master sheets.
|
| 118 |
+
|
| 119 |
+
- **Automated Timeline:** Dynamically loads the **2025 Master** and compares it against every single mapped master sheet ever uploaded to your HuggingFace dataset.
|
| 120 |
+
- **Missing Schools Tracker:** Automatically computes the difference in UDISE codes to show exactly which schools have disappeared or dropped off since 2025.
|
| 121 |
+
- **Privatization / Management Shifts:** Seamlessly merges historical data with new data to identify schools that have shifted from strictly Government management to Non-Government (Aided/Private) management.
|
| 122 |
+
- **Zero Web Scraping:** This tab operates entirely on your compiled HuggingFace data, requiring no active web scraping or internet crawling.
|
| 123 |
+
|
| 124 |
+
---
|
| 125 |
+
|
| 126 |
## 🛠️ Admin Scripts
|
| 127 |
|
| 128 |
If you need to make global, architectural changes to the baseline data outside of the UI, use the provided admin scripts:
|
USER_MANUAL.md
CHANGED
|
@@ -12,7 +12,8 @@ This manual provides a detailed, step-by-step guide to using every feature of th
|
|
| 12 |
3. [Tab 2: 📋 Master Sheet (Builder)](#3-tab-2--master-sheet-builder)
|
| 13 |
4. [Tab 3: 🗺️ Mapping Manager](#4-tab-3--mapping-manager)
|
| 14 |
5. [Tab 4: 📥 Download History](#5-tab-4--download-history)
|
| 15 |
-
6. [
|
|
|
|
| 16 |
|
| 17 |
---
|
| 18 |
|
|
@@ -129,7 +130,28 @@ This tab serves as your cloud backup and archive.
|
|
| 129 |
|
| 130 |
---
|
| 131 |
|
| 132 |
-
## 6.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
- **Do Not Interrupt the Scraper:** If you close the browser tab while Tab 1 is actively scraping, the process will stop. Let it run in the background.
|
| 135 |
- **Always Resolve Anomalies:** Never ignore the "New districts detected" table in Tab 2. If you force a build without resolving these, those schools will carry the newly scraped district names into your final database exactly as they are. You must ensure those new names match your Scholarship Application or rename them in the table before building.
|
|
|
|
| 12 |
3. [Tab 2: 📋 Master Sheet (Builder)](#3-tab-2--master-sheet-builder)
|
| 13 |
4. [Tab 3: 🗺️ Mapping Manager](#4-tab-3--mapping-manager)
|
| 14 |
5. [Tab 4: 📥 Download History](#5-tab-4--download-history)
|
| 15 |
+
6. [Tab 5: 📊 Master Analytics](#6-tab-5--master-analytics)
|
| 16 |
+
7. [Best Practices & Tips](#7-best-practices--tips)
|
| 17 |
|
| 18 |
---
|
| 19 |
|
|
|
|
| 130 |
|
| 131 |
---
|
| 132 |
|
| 133 |
+
## 6. Tab 5: 📊 Master Analytics
|
| 134 |
+
|
| 135 |
+
This tab serves as your multi-year historical analysis dashboard, providing high-level macro insights across your entire dataset over time without doing any active web scraping.
|
| 136 |
+
|
| 137 |
+
### 📈 Multi-Year Master Analytics
|
| 138 |
+
When you click **🔄 Run Analytics**, the system will:
|
| 139 |
+
1. Automatically pull your 2025 anchor master dataset.
|
| 140 |
+
2. Automatically pull *every single* mapped master sheet (`mapped_master_{date}.parquet`) you have ever compiled from HuggingFace.
|
| 141 |
+
3. Compare the newest master against the 2025 data.
|
| 142 |
+
|
| 143 |
+
### 📅 Master Timeline
|
| 144 |
+
A high-level historical breakdown showing exactly how the total number of schools (and Government schools) shifts from year to year.
|
| 145 |
+
|
| 146 |
+
### ⚠️ Missing Schools (vs 2025)
|
| 147 |
+
This table explicitly tracks the UDISE codes of schools that existed in 2025 but are completely absent from the newest compiled master sheet. It filters out duplicates automatically for precise counts.
|
| 148 |
+
|
| 149 |
+
### 🔄 Management Shifts (Government → Non-Govt)
|
| 150 |
+
By merging the datasets on their UDISE codes, this table instantly flags any school that was previously managed strictly by the Government in 2025 (e.g. Dept of Education, Panchayat) but has now shifted to an Aided, Private, or Unaided management structure in the newest master.
|
| 151 |
+
|
| 152 |
+
---
|
| 153 |
+
|
| 154 |
+
## 7. Best Practices & Tips
|
| 155 |
|
| 156 |
- **Do Not Interrupt the Scraper:** If you close the browser tab while Tab 1 is actively scraping, the process will stop. Let it run in the background.
|
| 157 |
- **Always Resolve Anomalies:** Never ignore the "New districts detected" table in Tab 2. If you force a build without resolving these, those schools will carry the newly scraped district names into your final database exactly as they are. You must ensure those new names match your Scholarship Application or rename them in the table before building.
|
app.py
CHANGED
|
@@ -1539,6 +1539,191 @@ with gr.Blocks(title="School Data Fetcher", css=css, theme=custom_theme) as app:
|
|
| 1539 |
dl_dropdown.change(fn=ui_prep_download, inputs=[dl_dropdown], outputs=[dl_download_btn, dl_status])
|
| 1540 |
|
| 1541 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1542 |
if __name__ == "__main__":
|
| 1543 |
if os.environ.get("SPACE_ID"):
|
| 1544 |
print("Starting on Dataset Spaces (0.0.0.0:7860) …")
|
|
|
|
| 1539 |
dl_dropdown.change(fn=ui_prep_download, inputs=[dl_dropdown], outputs=[dl_download_btn, dl_status])
|
| 1540 |
|
| 1541 |
|
| 1542 |
+
# ── Tab 5: Master Analytics ────────────────────────────────────────────
|
| 1543 |
+
with gr.Tab("📊 Master Analytics"):
|
| 1544 |
+
gr.HTML('''
|
| 1545 |
+
<div style="background:linear-gradient(135deg,rgba(16,185,129,0.08),rgba(14,165,233,0.08));
|
| 1546 |
+
border:1px solid rgba(16,185,129,0.2); border-radius:14px; padding:18px 22px; margin-bottom:18px;">
|
| 1547 |
+
<div style="font-weight:700; font-size:1.05em; margin-bottom:6px;">📈 Multi-Year Master Analytics</div>
|
| 1548 |
+
<div style="font-size:.9em; color:var(--body-text-color-subdued); line-height:1.7;">
|
| 1549 |
+
This tab automatically loads the <b>2025 master</b> and every
|
| 1550 |
+
<b>mapped master sheet</b> ever built. It analyzes how government school footprints and overall school counts shift year over year.
|
| 1551 |
+
</div>
|
| 1552 |
+
</div>
|
| 1553 |
+
''')
|
| 1554 |
+
|
| 1555 |
+
with gr.Row():
|
| 1556 |
+
analytics_refresh_btn = gr.Button("🔄 Run Analytics", variant="primary", scale=2)
|
| 1557 |
+
analytics_status = gr.HTML()
|
| 1558 |
+
|
| 1559 |
+
gr.HTML("<div class='section-label'>📅 Master Timeline</div>")
|
| 1560 |
+
gr.HTML("<div style='font-size:.85em;color:var(--body-text-color-subdued);margin-bottom:8px;'>Year-over-year changes across all master sheets compared to 2025.</div>")
|
| 1561 |
+
timeline_table = gr.Dataframe(label="Timeline Metrics", interactive=False, wrap=True)
|
| 1562 |
+
|
| 1563 |
+
gr.HTML("<div class='section-label'>⚠️ Missing Schools (vs 2025)</div>")
|
| 1564 |
+
missing_schools_html = gr.HTML()
|
| 1565 |
+
missing_schools_table = gr.Dataframe(label="Missing Schools Detailed", interactive=False, wrap=True)
|
| 1566 |
+
|
| 1567 |
+
gr.HTML("<div class='section-label'>🔄 Management Shifts</div>")
|
| 1568 |
+
mgmt_shifts_html = gr.HTML()
|
| 1569 |
+
mgmt_shifts_table = gr.Dataframe(label="Government → Non-Govt Shifts", interactive=False, wrap=True)
|
| 1570 |
+
|
| 1571 |
+
BASE_CSV_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "master_all_states.csv")
|
| 1572 |
+
|
| 1573 |
+
def _comp_detect_year_label(filepath: str) -> str:
|
| 1574 |
+
import calendar as _cal
|
| 1575 |
+
import re as _re
|
| 1576 |
+
import os as _os
|
| 1577 |
+
name = _os.path.basename(filepath or "")
|
| 1578 |
+
m = _re.search(r"(\d{4})[-_](\d{2})", name)
|
| 1579 |
+
if m:
|
| 1580 |
+
yr, mo = int(m.group(1)), int(m.group(2))
|
| 1581 |
+
if 1 <= mo <= 12:
|
| 1582 |
+
return f"{_cal.month_abbr[mo]} {yr}"
|
| 1583 |
+
m2 = _re.search(r"(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[_\-]?(\d{4})", name, _re.IGNORECASE)
|
| 1584 |
+
if m2:
|
| 1585 |
+
return f"{m2.group(1).capitalize()} {m2.group(2)}"
|
| 1586 |
+
m3 = _re.search(r"(\d{4})", name)
|
| 1587 |
+
return m3.group(1) if m3 else name
|
| 1588 |
+
|
| 1589 |
+
def _comp_find_col(df, *keywords):
|
| 1590 |
+
for k in keywords:
|
| 1591 |
+
for c in df.columns:
|
| 1592 |
+
if k in c.lower():
|
| 1593 |
+
return c
|
| 1594 |
+
return None
|
| 1595 |
+
|
| 1596 |
+
def ui_run_analytics():
|
| 1597 |
+
from hf_store import get_hf_credentials, list_mapped_master_files, pull_baseline_master
|
| 1598 |
+
from huggingface_hub import hf_hub_download
|
| 1599 |
+
import pandas as pd
|
| 1600 |
+
import os
|
| 1601 |
+
|
| 1602 |
+
token, repo = get_hf_credentials()
|
| 1603 |
+
if not token or not repo:
|
| 1604 |
+
return "<p style='color:#ef4444;'>⚠️ HF credentials not set.</p>", pd.DataFrame(), "", pd.DataFrame(), "", pd.DataFrame()
|
| 1605 |
+
|
| 1606 |
+
masters = []
|
| 1607 |
+
# 1. Load baseline
|
| 1608 |
+
try:
|
| 1609 |
+
base_df = pull_baseline_master(token, repo)
|
| 1610 |
+
if not base_df.empty:
|
| 1611 |
+
masters.append({"label": "2025", "df": base_df})
|
| 1612 |
+
except Exception:
|
| 1613 |
+
pass
|
| 1614 |
+
|
| 1615 |
+
# Fallback to CSV if HF fails
|
| 1616 |
+
if not masters and os.path.exists(BASE_CSV_PATH):
|
| 1617 |
+
masters.append({"label": "2025", "df": pd.read_csv(BASE_CSV_PATH, low_memory=False)})
|
| 1618 |
+
|
| 1619 |
+
# 2. Pull HF mapped masters
|
| 1620 |
+
hf_files = list_mapped_master_files(token, repo)
|
| 1621 |
+
for hf_path in sorted(hf_files):
|
| 1622 |
+
try:
|
| 1623 |
+
local = hf_hub_download(repo_id=repo, repo_type="dataset", filename=hf_path, token=token, force_download=True)
|
| 1624 |
+
label = _comp_detect_year_label(hf_path)
|
| 1625 |
+
masters.append({"label": label, "df": pd.read_parquet(local)})
|
| 1626 |
+
except Exception:
|
| 1627 |
+
pass
|
| 1628 |
+
|
| 1629 |
+
if not masters:
|
| 1630 |
+
return "<p style='color:#ef4444;'>⚠️ No master datasets found.</p>", pd.DataFrame(), "", pd.DataFrame(), "", pd.DataFrame()
|
| 1631 |
+
|
| 1632 |
+
def _norm_udise(s):
|
| 1633 |
+
return s.astype(str).str.strip().str.zfill(11)
|
| 1634 |
+
|
| 1635 |
+
def is_govt(s):
|
| 1636 |
+
val = str(s).lower()
|
| 1637 |
+
if 'aided' in val or 'private' in val or 'unaided' in val or 'un-aided' in val:
|
| 1638 |
+
return False
|
| 1639 |
+
return 'gov' in val or 'dept of edu' in val or 'local body' in val or 'panchayat' in val or 'municipal' in val
|
| 1640 |
+
|
| 1641 |
+
baseline = masters[0]
|
| 1642 |
+
b_udise = _comp_find_col(baseline["df"], "udise")
|
| 1643 |
+
b_mgmt = _comp_find_col(baseline["df"], "school_management_type", "management", "mgmt")
|
| 1644 |
+
if b_udise: baseline["df"]["_U"] = _norm_udise(baseline["df"][b_udise])
|
| 1645 |
+
if b_mgmt: baseline["df"]["is_govt"] = baseline["df"][b_mgmt].apply(is_govt)
|
| 1646 |
+
|
| 1647 |
+
timeline_data = []
|
| 1648 |
+
baseline_udises = set(baseline["df"]["_U"].dropna()) if b_udise else set()
|
| 1649 |
+
|
| 1650 |
+
missing_df = pd.DataFrame()
|
| 1651 |
+
changed_mgmt = pd.DataFrame()
|
| 1652 |
+
|
| 1653 |
+
for m in masters:
|
| 1654 |
+
m_df = m["df"]
|
| 1655 |
+
m_udise = _comp_find_col(m_df, "udise")
|
| 1656 |
+
m_mgmt = _comp_find_col(m_df, "school_management_type", "management", "mgmt")
|
| 1657 |
+
m_name = _comp_find_col(m_df, "school_name", "schoolname")
|
| 1658 |
+
m_state = _comp_find_col(m_df, "state")
|
| 1659 |
+
|
| 1660 |
+
if m_udise: m_df["_U"] = _norm_udise(m_df[m_udise])
|
| 1661 |
+
if m_mgmt: m_df["is_govt"] = m_df[m_mgmt].apply(is_govt)
|
| 1662 |
+
|
| 1663 |
+
tot_schools = len(m_df)
|
| 1664 |
+
tot_govt = m_df["is_govt"].sum() if m_mgmt else 0
|
| 1665 |
+
|
| 1666 |
+
missing_count = 0
|
| 1667 |
+
shifts_count = 0
|
| 1668 |
+
|
| 1669 |
+
if m != baseline and b_udise and m_udise:
|
| 1670 |
+
m_udises = set(m_df["_U"].dropna())
|
| 1671 |
+
missing = baseline_udises - m_udises
|
| 1672 |
+
missing_count = len(missing)
|
| 1673 |
+
|
| 1674 |
+
if m == masters[-1]:
|
| 1675 |
+
b_name = _comp_find_col(baseline["df"], "school_name", "schoolname")
|
| 1676 |
+
b_state = _comp_find_col(baseline["df"], "state")
|
| 1677 |
+
missing_df = baseline["df"][baseline["df"]["_U"].isin(missing)].drop_duplicates(subset=["_U"]).copy()
|
| 1678 |
+
|
| 1679 |
+
cols_to_keep = []
|
| 1680 |
+
for c in ["_U", b_name, b_state, b_mgmt]:
|
| 1681 |
+
if c and c in missing_df.columns:
|
| 1682 |
+
cols_to_keep.append(c)
|
| 1683 |
+
missing_df = missing_df[cols_to_keep] if cols_to_keep else missing_df
|
| 1684 |
+
missing_df.insert(0, "Missing In Master", m["label"])
|
| 1685 |
+
|
| 1686 |
+
if b_mgmt and m_mgmt:
|
| 1687 |
+
b_cols = [c for c in ["_U", b_name, b_mgmt, "is_govt"] if c]
|
| 1688 |
+
m_cols = [c for c in ["_U", m_name, m_mgmt, "is_govt"] if c]
|
| 1689 |
+
|
| 1690 |
+
merged = pd.merge(
|
| 1691 |
+
baseline["df"][b_cols],
|
| 1692 |
+
m_df[m_cols],
|
| 1693 |
+
on="_U", suffixes=("_old", "_new")
|
| 1694 |
+
)
|
| 1695 |
+
changed = merged[(merged["is_govt_old"] == True) & (merged["is_govt_new"] == False)]
|
| 1696 |
+
shifts_count = len(changed)
|
| 1697 |
+
|
| 1698 |
+
c_name_old = str(b_name) + '_old' if b_name == m_name else b_name
|
| 1699 |
+
c_mgmt_old = str(b_mgmt) + '_old' if b_mgmt == m_mgmt else b_mgmt
|
| 1700 |
+
c_mgmt_new = str(m_mgmt) + '_new' if b_mgmt == m_mgmt else m_mgmt
|
| 1701 |
+
|
| 1702 |
+
c_cols = [c for c in ["_U", c_name_old, c_mgmt_old, c_mgmt_new] if c and c in changed.columns]
|
| 1703 |
+
changed_mgmt = changed[c_cols] if c_cols else changed
|
| 1704 |
+
changed_mgmt.insert(0, "Shifted In Master", m["label"])
|
| 1705 |
+
|
| 1706 |
+
timeline_data.append({
|
| 1707 |
+
"Master Name": m["label"],
|
| 1708 |
+
"Total Schools": f"{tot_schools:,}",
|
| 1709 |
+
"Total Govt Schools": f"{tot_govt:,}",
|
| 1710 |
+
"Missing (vs 2025)": f"{missing_count:,}" if m != baseline else "-",
|
| 1711 |
+
"Govt->Non-Govt Shifts": f"{shifts_count:,}" if m != baseline else "-"
|
| 1712 |
+
})
|
| 1713 |
+
|
| 1714 |
+
timeline_df = pd.DataFrame(timeline_data)
|
| 1715 |
+
|
| 1716 |
+
status_html = f"<p style='color:#22c55e;font-weight:600;'>✅ Loaded <b>{len(masters)}</b> master datasets successfully.</p>"
|
| 1717 |
+
m_html = f"<div style='font-size:1.1em; margin-top:10px;'><b>{len(missing_df):,}</b> schools from 2025 are completely missing in the newest master.</div>"
|
| 1718 |
+
c_html = f"<div style='font-size:1.1em; margin-top:10px;'><b>{len(changed_mgmt):,}</b> schools shifted from Govt to Non-Govt management since 2025.</div>"
|
| 1719 |
+
|
| 1720 |
+
return status_html, timeline_df, m_html, missing_df, c_html, changed_mgmt
|
| 1721 |
+
|
| 1722 |
+
analytics_refresh_btn.click(
|
| 1723 |
+
fn=ui_run_analytics,
|
| 1724 |
+
outputs=[analytics_status, timeline_table, missing_schools_html, missing_schools_table, mgmt_shifts_html, mgmt_shifts_table]
|
| 1725 |
+
)
|
| 1726 |
+
|
| 1727 |
if __name__ == "__main__":
|
| 1728 |
if os.environ.get("SPACE_ID"):
|
| 1729 |
print("Starting on Dataset Spaces (0.0.0.0:7860) …")
|
master_analysis.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
def is_govt(management_name):
|
| 5 |
+
"""Simple function to classify if a school is strictly Government managed"""
|
| 6 |
+
val = str(management_name).lower()
|
| 7 |
+
if 'aided' in val or 'private' in val or 'unaided' in val or 'un-aided' in val:
|
| 8 |
+
return False
|
| 9 |
+
return 'gov' in val or 'dept of edu' in val or 'local body' in val or 'panchayat' in val or 'municipal' in val
|
| 10 |
+
|
| 11 |
+
def run_analysis():
|
| 12 |
+
# Define paths (Update the parquet path to point to a real file when testing)
|
| 13 |
+
csv_path = "master_all_states.csv"
|
| 14 |
+
parquet_path = "mapped_master_2026_01.parquet" # Replace with your actual parquet file path
|
| 15 |
+
|
| 16 |
+
if not os.path.exists(csv_path) or not os.path.exists(parquet_path):
|
| 17 |
+
print(f"Make sure both {csv_path} and {parquet_path} exist in this folder to run the script!")
|
| 18 |
+
return
|
| 19 |
+
|
| 20 |
+
# 1. Load the 2025 dataset and the Newest dataset
|
| 21 |
+
print("Loading datasets...")
|
| 22 |
+
df_2025 = pd.read_csv(csv_path, low_memory=False)
|
| 23 |
+
df_new = pd.read_parquet(parquet_path)
|
| 24 |
+
|
| 25 |
+
# 2. Normalize UDISE codes to ensure perfect 11-digit matching
|
| 26 |
+
df_2025['_U'] = df_2025['School_Udise_Code__c'].astype(str).str.strip().str.zfill(11)
|
| 27 |
+
df_new['_U'] = df_new['School_Udise_Code__c'].astype(str).str.strip().str.zfill(11)
|
| 28 |
+
|
| 29 |
+
# 3. Apply Government Flag using the strict School_Management_Type__c column
|
| 30 |
+
df_2025['is_govt'] = df_2025['School_Management_Type__c'].apply(is_govt)
|
| 31 |
+
df_new['is_govt'] = df_new['School_Management_Type__c'].apply(is_govt)
|
| 32 |
+
|
| 33 |
+
print(f"Total Govt Schools (2025): {df_2025['is_govt'].sum():,}")
|
| 34 |
+
print(f"Total Govt Schools (New): {df_new['is_govt'].sum():,}")
|
| 35 |
+
|
| 36 |
+
# ---------------------------------------------------------
|
| 37 |
+
# FINDING MISSING SCHOOLS
|
| 38 |
+
# ---------------------------------------------------------
|
| 39 |
+
udises_2025 = set(df_2025['_U'].dropna())
|
| 40 |
+
udises_new = set(df_new['_U'].dropna())
|
| 41 |
+
missing_udises = udises_2025 - udises_new
|
| 42 |
+
|
| 43 |
+
# Filter 2025 data to only show missing schools, and DROP duplicates for accurate counts!
|
| 44 |
+
missing_df = df_2025[df_2025['_U'].isin(missing_udises)].drop_duplicates(subset=['_U']).copy()
|
| 45 |
+
|
| 46 |
+
# Add Master Year column
|
| 47 |
+
missing_df.insert(0, "Missing In Master", "2026")
|
| 48 |
+
|
| 49 |
+
print(f"\nTotal Missing Schools: {len(missing_df)}")
|
| 50 |
+
print("Missing Schools by State:\n", missing_df['School_State__c'].value_counts().head())
|
| 51 |
+
|
| 52 |
+
# ---------------------------------------------------------
|
| 53 |
+
# FINDING MANAGEMENT SHIFTS (Govt -> Non-Govt)
|
| 54 |
+
# ---------------------------------------------------------
|
| 55 |
+
# Merge the two datasets on UDISE code
|
| 56 |
+
merged = pd.merge(
|
| 57 |
+
df_2025[['_U', 'School_Name__c', 'School_Management_Type__c', 'is_govt']],
|
| 58 |
+
df_new[['_U', 'School_Name__c', 'School_Management_Type__c', 'is_govt']],
|
| 59 |
+
on='_U',
|
| 60 |
+
suffixes=('_old', '_new')
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
# Filter where it WAS govt, but is NO LONGER govt
|
| 64 |
+
changed_mgmt = merged[(merged['is_govt_old'] == True) & (merged['is_govt_new'] == False)].copy()
|
| 65 |
+
changed_mgmt.insert(0, "Shifted In Master", "2026")
|
| 66 |
+
|
| 67 |
+
print(f"\nSchools shifted from Govt -> Non-Govt: {len(changed_mgmt)}")
|
| 68 |
+
return missing_df, changed_mgmt
|
| 69 |
+
|
| 70 |
+
if __name__ == "__main__":
|
| 71 |
+
run_analysis()
|