sharanyaswarup commited on
Commit
6f32bed
·
1 Parent(s): dea1497

feat: added multi-year Master Analytics tab & updated documentation

Browse files
Files changed (4) hide show
  1. README.md +12 -1
  2. USER_MANUAL.md +24 -2
  3. app.py +185 -0
  4. 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 **4 main tabs**.
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. [Best Practices & Tips](#6-best-practices--tips)
 
16
 
17
  ---
18
 
@@ -129,7 +130,28 @@ This tab serves as your cloud backup and archive.
129
 
130
  ---
131
 
132
- ## 6. Best Practices & Tips
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()