import streamlit as st
import pandas as pd
import folium
from folium.plugins import MarkerCluster, Search
from streamlit_folium import st_folium
import html
import io
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# --- Configuración ---
st.set_page_config(page_title="Puntajes SIMCE 2024 - Centros Escolares", page_icon=":school:", layout="wide")
st.title(":school: Puntajes SIMCE 2024 - 2do Medio")
st.markdown("Explora los puntajes SIMCE de los establecimientos educacionales en Chile (Datos del MINEDUC)")
# --- Diccionarios para mapear valores a descripciones ---
dependencia_map = {
1: 'Municipal Corporación',
2: 'Municipal DAEM',
3: 'Particular subvencionado',
4: 'Particular pagado',
5: 'Corporación de administración delegada',
6: 'Servicio Local de Educación'
}
socioecon_map = {
1: 'Bajo',
2: 'Medio Bajo',
3: 'Medio',
4: 'Medio Alto',
5: 'Alto'
}
rural_map = {
1: 'Urbano',
2: 'Rural'
}
# --- Cargar datos ---
@st.cache_data
def load_data():
df = pd.read_csv(
"src/Data/simce.csv",
sep=";",
decimal=",",
encoding="ISO-8859-1",
header=0,
on_bad_lines="skip"
)
# Crear columnas con descripciones
df['dependencia_desc'] = df['dependencia'].map(dependencia_map)
df['grupo_socioecon_desc'] = df['grupo_socioecon'].map(socioecon_map)
df['es_rural_desc'] = df['es_rural'].map(rural_map)
return df
# Cargar datos
df = load_data()
# --- Filtros ---
st.subheader("Filtros de Búsqueda")
col1, col2, col3, col4 = st.columns(4)
regiones = ["(Todas)"] + sorted(df["nombre_region"].dropna().unique().tolist())
ruralidades = ["(Todas)"] + sorted(df["es_rural_desc"].dropna().unique().tolist())
dependencias = ["(Todas)"] + sorted(df["dependencia_desc"].dropna().unique().tolist())
socioecons = ["(Todas)"] + sorted(df["grupo_socioecon_desc"].dropna().unique().tolist())
with col1:
sel_region = st.selectbox("Región", regiones, index=0, help="Selecciona una región para filtrar los establecimientos.")
with col2:
sel_ruralidad = st.selectbox("Ruralidad", ruralidades, index=0, help="Filtra por tipo de área (urbana o rural).")
with col3:
sel_dependencia = st.selectbox("Dependencia", dependencias, index=0, help="Filtra por tipo de establecimiento.")
with col4:
sel_socioecon = st.selectbox("Grupo Socioeconómico", socioecons, index=0, help="Filtra por nivel socioeconómico.")
# Filtrar datos
df_f = df.copy()
if sel_region != "(Todas)":
df_f = df_f[df_f["nombre_region"] == sel_region]
if sel_ruralidad != "(Todas)":
df_f = df_f[df_f["es_rural_desc"] == sel_ruralidad]
if sel_dependencia != "(Todas)":
df_f = df_f[df_f["dependencia_desc"] == sel_dependencia]
if sel_socioecon != "(Todas)":
df_f = df_f[df_f["grupo_socioecon_desc"] == sel_socioecon]
# --- Nueva sección: Comparación de Colegios por Percentiles ---
st.subheader("Comparación de Colegio por Percentiles")
st.markdown("Selecciona un colegio para comparar sus puntajes en Matemáticas y Lenguaje con respecto a los demás colegios filtrados.")
# Función para calcular percentiles
def calcular_percentiles(colegio, df_filtrado):
# Filtrar datos válidos (sin NaN)
df_math = df_filtrado[df_filtrado['promedio_matematica'].notna()]
df_lectura = df_filtrado[df_filtrado['promedio_lectura'].notna()]
# Obtener puntajes del colegio seleccionado
math_score = colegio['promedio_matematica'].values[0]
lectura_score = colegio['promedio_lectura'].values[0]
# Calcular percentiles
if not df_math.empty and not pd.isna(math_score):
percentil_math = (sum(df_math['promedio_matematica'] <= math_score) / len(df_math)) * 100
else:
percentil_math = None
if not df_lectura.empty and not pd.isna(lectura_score):
percentil_lectura = (sum(df_lectura['promedio_lectura'] <= lectura_score) / len(df_lectura)) * 100
else:
percentil_lectura = None
return percentil_math, percentil_lectura
# Función para crear boxplot individual
def crear_boxplot_individual(datos, colegio_puntaje, titulo, color, nombre_colegio):
if datos.empty:
return None
# Crear figura
fig, ax = plt.subplots(figsize=(10, 6))
# Crear boxplot
boxplot = ax.boxplot(datos, patch_artist=True, widths=0.6)
# Colorear el boxplot
boxplot['boxes'][0].set_facecolor(color)
# Marcar la posición del colegio seleccionado
ax.plot(1, colegio_puntaje, 'ro', markersize=10, label=f'{nombre_colegio}: {colegio_puntaje:.1f}')
# Personalizar
ax.set_title(titulo, fontsize=16, fontweight='bold')
ax.set_ylabel('Puntaje', fontsize=12)
ax.set_xticklabels(['']) # Ocultar label del eje x
ax.legend(loc='upper right')
# Añadir líneas de referencia para percentiles
q1, median, q3 = np.percentile(datos, [25, 50, 75])
ax.axhline(y=median, color='green', linestyle='--', alpha=0.7, label=f'Mediana: {median:.1f}')
# Añadir estadísticas descriptivas
stats_text = f"""
N: {len(datos)}
Mín: {datos.min():.1f}
Q1: {q1:.1f}
Mediana: {median:.1f}
Q3: {q3:.1f}
Máx: {datos.max():.1f}
"""
ax.text(0.02, 0.98, stats_text, transform=ax.transAxes, verticalalignment='top',
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5), fontsize=10)
plt.tight_layout()
return fig
# Función para generar texto comparativo
def generar_texto_comparativo(colegio_data, df_filtrados, df_lista_manual, materia):
nombre_colegio = colegio_data['nombre_colegio'].values[0]
puntaje_colegio = colegio_data[materia].values[0]
texto = f"### Comparación de {materia.replace('promedio_', '').capitalize()} para {nombre_colegio}\n\n"
# Comparación con todos los colegios filtrados
datos_filtrados = df_filtrados[df_filtrados[materia].notna()][materia]
if not datos_filtrados.empty:
percentil_filtrados = (sum(datos_filtrados <= puntaje_colegio) / len(datos_filtrados)) * 100
mediana_filtrados = np.median(datos_filtrados)
diferencia_filtrados = puntaje_colegio - mediana_filtrados
texto += f"**Comparación con todos los colegios filtrados ({len(datos_filtrados)} colegios):**\n"
texto += f"- Puntaje: {puntaje_colegio:.1f}\n"
texto += f"- Percentil: {percentil_filtrados:.1f}% (supera al {percentil_filtrados:.1f}% de los colegios)\n"
texto += f"- Diferencia con la mediana: {diferencia_filtrados:+.1f} puntos\n"
texto += f"- Mediana de todos los colegios: {mediana_filtrados:.1f}\n\n"
# Comparación con la lista manual si existe
if not df_lista_manual.empty:
datos_lista = df_lista_manual[df_lista_manual[materia].notna()][materia]
if not datos_lista.empty:
percentil_lista = (sum(datos_lista <= puntaje_colegio) / len(datos_lista)) * 100
mediana_lista = np.median(datos_lista)
diferencia_lista = puntaje_colegio - mediana_lista
texto += f"**Comparación con la lista de colegios seleccionados ({len(datos_lista)} colegios):**\n"
texto += f"- Puntaje: {puntaje_colegio:.1f}\n"
texto += f"- Percentil: {percentil_lista:.1f}% (supera al {percentil_lista:.1f}% de los colegios de la lista)\n"
texto += f"- Diferencia con la mediana: {diferencia_lista:+.1f} puntos\n"
texto += f"- Mediana de la lista: {mediana_lista:.1f}\n\n"
return texto
# Seleccionar un colegio para comparar
colegios_filtrados = sorted(df_f['nombre_colegio'].unique().tolist())
colegio_seleccionado = st.selectbox(
"Selecciona un colegio para analizar",
["(Selecciona un colegio)"] + colegios_filtrados,
index=0,
help="Selecciona un colegio para ver su posición percentil en Matemáticas y Lenguaje."
)
if colegio_seleccionado != "(Selecciona un colegio)":
# Obtener datos del colegio seleccionado
colegio_data = df_f[df_f['nombre_colegio'] == colegio_seleccionado]
if not colegio_data.empty:
# Calcular percentiles
percentil_math, percentil_lectura = calcular_percentiles(colegio_data, df_f)
# Mostrar resultados
col1, col2 = st.columns(2)
with col1:
if percentil_math is not None:
st.metric(
label=f"Percentil en Matemáticas",
value=f"{percentil_math:.1f}%",
help=f"Este colegio supera al {percentil_math:.1f}% de los colegios filtrados en Matemáticas."
)
st.progress(percentil_math/100)
else:
st.warning("No hay datos de Matemáticas para este colegio.")
with col2:
if percentil_lectura is not None:
st.metric(
label=f"Percentil en Lenguaje",
value=f"{percentil_lectura:.1f}%",
help=f"Este colegio supera al {percentil_lectura:.1f}% de los colegios filtrados en Lenguaje."
)
st.progress(percentil_lectura/100)
else:
st.warning("No hay datos de Lenguaje para este colegio.")
# Mostrar información detallada del colegio
st.subheader(f"Información detallada de {colegio_seleccionado}")
colegio_info = colegio_data.iloc[0]
info_cols = st.columns(2)
with info_cols[0]:
st.write(f"**Región:** {colegio_info['nombre_region']}")
st.write(f"**Comuna:** {colegio_info['nombre_comuna']}")
st.write(f"**Dependencia:** {colegio_info['dependencia_desc']}")
with info_cols[1]:
st.write(f"**Grupo Socioeconómico:** {colegio_info['grupo_socioecon_desc']}")
st.write(f"**Ruralidad:** {colegio_info['es_rural_desc']}")
if percentil_math is not None and percentil_lectura is not None:
st.write(f"**Puntaje promedio:** Matemáticas={colegio_info['promedio_matematica']:.1f}, Lenguaje={colegio_info['promedio_lectura']:.1f}")
# --- Nueva sección: Crear lista de colegios ---
st.subheader("Crear Lista de Colegios")
st.markdown("Busca colegios por palabra clave en el nombre y selecciona para crear tu lista personalizada.")
# Inicializar estado de sesión para los colegios seleccionados
if 'selected_colegios' not in st.session_state:
st.session_state.selected_colegios = []
# Input para la palabra clave
keyword = st.text_input("Palabra clave en el nombre del colegio", help="Ingresa una palabra para filtrar los nombres de colegios que la contengan (insensible a mayúsculas/minúsculas).")
# Filtrar colegios basados en la palabra clave
if keyword:
df_search = df_f[df_f['nombre_colegio'].str.contains(keyword, case=False, na=False)]
else:
df_search = df_f
# Obtener lista única de nombres de colegios filtrados
colegios_disponibles = sorted(df_search['nombre_colegio'].unique().tolist())
# Combinar colegios seleccionados previamente con los disponibles en el filtro actual
# Solo incluir colegios que aún estén en df_f (después de aplicar filtros de región, ruralidad, etc.)
combined_colegios = sorted(set(colegios_disponibles + [c for c in st.session_state.selected_colegios if c in df_f['nombre_colegio'].values]))
# Multiselect para seleccionar colegios, con los previamente seleccionados como predeterminados
selected_colegios = st.multiselect(
"Selecciona colegios para tu lista",
combined_colegios,
default=[c for c in st.session_state.selected_colegios if c in combined_colegios],
help="Selecciona múltiples colegios de la lista filtrada. Los colegios seleccionados previamente se mantienen."
)
# Actualizar el estado de sesión con las nuevas selecciones
st.session_state.selected_colegios = selected_colegios
# Crear dataframe de colegios seleccionados
df_selected = df_f[df_f['nombre_colegio'].isin(st.session_state.selected_colegios)]
# --- Mostrar comparación detallada y boxplots separados ---
if colegio_seleccionado != "(Selecciona un colegio)":
st.subheader("Análisis Comparativo Detallado")
# Generar y mostrar texto comparativo para Matemáticas
texto_math = generar_texto_comparativo(colegio_data, df_f, df_selected, 'promedio_matematica')
st.markdown(texto_math)
# Crear y mostrar boxplots individuales para Matemáticas
math_filtrados = df_f[df_f['promedio_matematica'].notna()]['promedio_matematica']
math_lista = df_selected[df_selected['promedio_matematica'].notna()]['promedio_matematica']
math_puntaje = colegio_data['promedio_matematica'].values[0]
col_math1, col_math2 = st.columns(2)
with col_math1:
if not math_filtrados.empty:
math_boxplot_filtrados = crear_boxplot_individual(
math_filtrados, math_puntaje,
'Distribución de Matemáticas - Todos los Colegios Filtrados',
'lightblue', colegio_seleccionado
)
st.pyplot(math_boxplot_filtrados)
with col_math2:
if not df_selected.empty and not math_lista.empty:
math_boxplot_lista = crear_boxplot_individual(
math_lista, math_puntaje,
'Distribución de Matemáticas - Lista de Colegios Seleccionados',
'lightgreen', colegio_seleccionado
)
st.pyplot(math_boxplot_lista)
# Generar y mostrar texto comparativo para Lenguaje
st.markdown("---")
texto_lectura = generar_texto_comparativo(colegio_data, df_f, df_selected, 'promedio_lectura')
st.markdown(texto_lectura)
# Crear y mostrar boxplots individuales para Lenguaje
lectura_filtrados = df_f[df_f['promedio_lectura'].notna()]['promedio_lectura']
lectura_lista = df_selected[df_selected['promedio_lectura'].notna()]['promedio_lectura']
lectura_puntaje = colegio_data['promedio_lectura'].values[0]
col_lectura1, col_lectura2 = st.columns(2)
with col_lectura1:
if not lectura_filtrados.empty:
lectura_boxplot_filtrados = crear_boxplot_individual(
lectura_filtrados, lectura_puntaje,
'Distribución de Lenguaje - Todos los Colegios Filtrados',
'lightblue', colegio_seleccionado
)
st.pyplot(lectura_boxplot_filtrados)
with col_lectura2:
if not df_selected.empty and not lectura_lista.empty:
lectura_boxplot_lista = crear_boxplot_individual(
lectura_lista, lectura_puntaje,
'Distribución de Lenguaje - Lista de Colegios Seleccionados',
'lightgreen', colegio_seleccionado
)
st.pyplot(lectura_boxplot_lista)
# Usar df_selected si hay colegios seleccionados, de lo contrario usar df_f
df_map = df_selected if not df_selected.empty else df_f
# Filtrar filas con coordenadas válidas
df_map = df_map.dropna(subset=["latitud", "longitud"])
# --- Estadísticas resumidas ---
st.subheader("Estadísticas Resumidas")
if not df_map.empty:
col_stats1, col_stats2 = st.columns(2)
with col_stats1:
st.metric("Número de Establecimientos", len(df_map))
st.metric("Promedio Lenguaje", f"{df_map['promedio_lectura'].mean():.1f}")
with col_stats2:
st.metric("Promedio Matemáticas", f"{df_map['promedio_matematica'].mean():.1f}")
st.metric("Regiones Cubiertas", df_map["nombre_region"].nunique())
else:
st.warning("No hay datos disponibles con los filtros seleccionados o no se han seleccionado colegios.")
# --- Mapeo de colores por tipo ---
color_map = {
'Municipal Corporación': 'blue',
'Municipal DAEM': 'green',
'Particular subvencionado': 'orange',
'Particular pagado': 'purple',
'Corporación de administración delegada': 'red',
'Servicio Local de Educación': 'cadetblue'
}
# Función para asignar color según tipo
def tipo_color(tipo: str) -> str:
return color_map.get(tipo, "gray")
# --- Crear mapa centrado en Chile ---
m = folium.Map(location=[-33.45, -70.65], zoom_start=6, tiles="CartoDB positron")
# Cluster
cluster = MarkerCluster().add_to(m)
# --- Agregar búsqueda por nombre de colegio ---
school_search = Search(
layer=cluster,
search_label="nombre_colegio",
placeholder="Buscar por nombre del colegio...",
collapsed=False,
).add_to(m)
# --- Agregar marcadores ---
for _, r in df_map.iterrows():
lat, lon = float(r["latitud"]), float(r["longitud"])
nombre = html.escape(str(r.get("nombre_colegio", "")))
comuna = html.escape(str(r.get("nombre_comuna", "")))
tipo = html.escape(str(r.get("dependencia_desc", "")))
lenguaje = html.escape(str(r.get("promedio_lectura", "")))
matematica = html.escape(str(r.get("promedio_matematica", "")))
rural = html.escape(str(r.get("es_rural_desc", "")))
grupoeconomico = html.escape(str(r.get("grupo_socioecon_desc", "")))
popup_html = f"""
{nombre}
Tipo: {tipo}
Comuna: {comuna}
Promedio Lenguaje: {lenguaje}
Promedio Matemáticas: {matematica}
Es Rural: {rural}
Grupo Socioeconómico: {grupoeconomico}
"""
marker = folium.Marker(
location=[lat, lon],
popup=folium.Popup(popup_html, max_width=350),
icon=folium.Icon(color=tipo_color(r.get("dependencia_desc")), icon="plus", prefix="fa"),
)
marker.add_to(cluster)
marker.properties = {"nombre_colegio": nombre} # Para búsqueda
# --- Leyenda mejorada ---
legend_html = """