import pandas as pd
import streamlit as st
import folium
from streamlit_folium import folium_static
from colour import Color
import branca.colormap as cm
# Map Style Definitions
MAP_STYLES = {
"Uses standard OpenStreetMap": "OpenStreetMap",
"Carto Light": "CartoDB positron",
"Carto Dark": "CartoDB dark_matter",
"Carto Voyager": "CartoDB Voyager",
"Esri Gray Canvas": None, # Not directly available in Folium
"OSM HOT": None, # Not directly available in Folium
"Esri NatGeo": "Esri.NatGeoWorldMap",
"Esri Satellite": "Esri.WorldImagery",
"Esri National Geographic": "Esri.WorldStreetMap",
"OpenTopoMap Topography": "OpenTopoMap"
}
# ----------------------------
# 1. Data Loading and Preprocessing
# ----------------------------
@st.cache_data
def load_data():
# Read school data
df_schools = pd.read_csv("schools.csv")
# Filter private schools (case insensitive)
df_schools = df_schools[
~df_schools["description"].str.contains("private", case=False, na=False)
]
# Extract city name (remove ", TX")
df_schools["city"] = df_schools["city_state"].str.replace(r",\s*TX$", "", regex=True)
# Remove rows with missing rankings
df_schools = df_schools.dropna(subset=["rank_state_elementary"])
# Ensure ranking is numeric
df_schools["rank_state_elementary"] = pd.to_numeric(df_schools["rank_state_elementary"], errors="coerce")
df_schools = df_schools.dropna(subset=["rank_state_elementary"])
# Calculate city ranking (lower is better)
df_schools["rank_city"] = (
df_schools.groupby("city")["rank_state_elementary"]
.rank(method="min", ascending=True)
)
# Read city coordinates (from simplemaps.com free version)
df_cities = pd.read_csv("uscities.csv")
tx_cities = df_cities[df_cities["state_id"] == "TX"][["city", "lat", "lng"]].copy()
tx_cities["city"] = tx_cities["city"].str.title()
# Merge coordinates to school data (for Top3 mode)
df_schools = df_schools.merge(tx_cities, on="city", how="inner")
# Precompute "All" mode: city school count
school_counts = df_schools.groupby("city").size().reset_index(name="school_count")
merged_all = school_counts.merge(tx_cities, on="city", how="inner")
return df_schools, merged_all
# ----------------------------
# 2. Color Function (for All mode)
# ----------------------------
def get_color_count(value, min_val, max_val):
if min_val == max_val:
ratio = 0.5
else:
ratio = (value - min_val) / (max_val - min_val)
start_color = Color("#4B0082") # Dark Purple (Low values)
end_color = Color("#FFD700") # Bright Yellow (High values)
return list(start_color.range_to(end_color, 100))[int(ratio * 99)].hex
# ----------------------------
# 3. Map Creation Functions
# ----------------------------
def create_all_mode_map(df_all, map_style_name):
# Get the tile URL based on selection
tile_url = MAP_STYLES.get(map_style_name, "CartoDB Voyager")
# Handle special cases for map styles not directly supported in Folium
if tile_url is None:
tile_url = "CartoDB Voyager"
# Create base map centered on Texas
m = folium.Map(
location=[31.9686, -99.9018],
zoom_start=6,
tiles=tile_url
)
if df_all.empty:
return m
# Calculate min and max for color scaling
min_count = int(df_all["school_count"].min())
max_count = int(df_all["school_count"].max())
# Create color map legend
colormap = cm.LinearColormap(
colors=['#4B0082', '#FFD700'],
vmin=min_count,
vmax=max_count,
caption='School Count'
)
m.add_child(colormap)
# Radius Size Range Configuration For All Cities
MIN_RADIUS = 3 # Minimum radius for cities with few schools
MAX_RADIUS = 35 # Maximum radius for cities like Houston
# Add markers for each city
for _, row in df_all.iterrows():
count = row['school_count']
# Dynamic Radius Size Calculation For All Cities
if max_count == min_count:
radius = 10
else:
# Linear interpolation for size
norm = (count - min_count) / (max_count - min_count)
radius = MIN_RADIUS + (norm * (MAX_RADIUS - MIN_RADIUS))
# Get color based on school count
color = get_color_count(count, min_count, max_count)
# Add circle marker
folium.CircleMarker(
location=[row["lat"], row["lng"]],
radius=radius,
color="white",
weight=1,
fillColor=color,
fillOpacity=0.6,
popup=f"{row['city']}: {int(row['school_count'])} school(s)"
).add_to(m)
return m
def create_top3_mode_map(df_schools, map_style_name):
# Get the tile URL based on selection
tile_url = MAP_STYLES.get(map_style_name, "CartoDB Voyager")
# Handle special cases for map styles not directly supported in Folium
if tile_url is None:
tile_url = "CartoDB Voyager"
# Create base map centered on Texas
m = folium.Map(
location=[31.9686, -99.9018],
zoom_start=6,
tiles=tile_url
)
if df_schools.empty:
return m
# Get top 3 schools per city (highest state ranking)
top3_df = (
df_schools.groupby("city", group_keys=False)
.apply(lambda g: g.nsmallest(3, "rank_state_elementary"))
.reset_index(drop=True)
)
# Add markers for each city
for city, city_df in top3_df.groupby("city"):
# Sort by city ranking to ensure Top3 order
city_df = city_df.sort_values("rank_city").head(3)
lat, lng = city_df["lat"].iloc[0], city_df["lng"].iloc[0]
# Create popup HTML
popup_html = f"""
<div>
<strong>{city} (Top 3)</strong>
<br>
"""
for _, row in city_df.iterrows():
rank_city_int = int(row["rank_city"])
rank_state_int = int(row["rank_state_elementary"])
popup_html += f"""
<br>
🏆 Top {rank_city_int}: <strong>{row['school_name']}</strong> | TX Rank #{rank_state_int}
"""
popup_html += "</div>"
# Add marker
folium.CircleMarker(
location=[lat, lng],
radius=12,
color="black",
weight=2,
fillColor="#FF8C00",
fillOpacity=0.8,
popup=folium.Popup(popup_html, max_width=300)
).add_to(m)
return m
# ----------------------------
# 4. Streamlit App
# ----------------------------
def main():
# Page title
st.title("Texas Elementary Schools")
# Load data
df_schools, merged_all = load_data()
# Control Panel (View Selector + Map Style Selector)
col1, col2 = st.columns(2)
with col1:
# View Mode
view_mode = st.selectbox(
"Data View:",
["Overview (Bubble Map)", "Detailed (Top 3 Schools)"],
index=0
)
with col2:
# Map Background Style
map_style_name = st.selectbox(
"Map Background:",
list(MAP_STYLES.keys()),
index=2 # Default to Carto Voyager
)
# Create and display map based on view mode
if view_mode == "Overview (Bubble Map)":
st.header("Overview - School Count by City")
m = create_all_mode_map(merged_all, map_style_name)
folium_static(m, width=700, height=500)
else:
st.header("Detailed - Top 3 Schools by City")
m = create_top3_mode_map(df_schools, map_style_name)
folium_static(m, width=700, height=500)
if __name__ == "__main__":
main()