Carga inicial
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.venv/
|
||||||
|
test/
|
||||||
|
.streamlit/secrets.toml
|
||||||
29
.streamlit/config.toml
Normal file
29
.streamlit/config.toml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# for local development
|
||||||
|
[server]
|
||||||
|
|
||||||
|
#no default browser
|
||||||
|
headless = true
|
||||||
|
|
||||||
|
# for local development
|
||||||
|
runOnSave = true
|
||||||
|
|
||||||
|
# Limit uploads (MB). Excel files here are small; adjust if needed
|
||||||
|
maxUploadSize = 5
|
||||||
|
|
||||||
|
# for local development
|
||||||
|
fileWatcherType="poll"
|
||||||
|
|
||||||
|
|
||||||
|
[theme]
|
||||||
|
base="light"
|
||||||
|
primaryColor = "#001d68"
|
||||||
|
backgroundColor = "#FFFFFF"
|
||||||
|
secondaryBackgroundColor = "#F4F6FA"
|
||||||
|
textColor = "#111827"
|
||||||
|
font = "sans serif"
|
||||||
|
|
||||||
|
[client]
|
||||||
|
toolbarMode = "minimal"
|
||||||
|
|
||||||
|
[ui]
|
||||||
|
hideTopBar = true
|
||||||
49
.streamlit/config_prod.toml
Normal file
49
.streamlit/config_prod.toml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
## Streamlit config — Production defaults
|
||||||
|
|
||||||
|
[server]
|
||||||
|
# Run without launching a browser
|
||||||
|
headless = true
|
||||||
|
|
||||||
|
# Listen on all interfaces (useful for containers/VMs)
|
||||||
|
address = "0.0.0.0"
|
||||||
|
|
||||||
|
# Fixed port; override with STREAMLIT_SERVER_PORT if needed
|
||||||
|
port = 8501
|
||||||
|
|
||||||
|
# Behind a reverse proxy you typically want CORS disabled
|
||||||
|
enableCORS = false
|
||||||
|
|
||||||
|
# Keep XSRF protection ON in production
|
||||||
|
enableXsrfProtection = true
|
||||||
|
|
||||||
|
# Limit uploads (MB). Excel files here are small; adjust if needed
|
||||||
|
maxUploadSize = 5
|
||||||
|
|
||||||
|
# Reduce CPU usage by disabling file watching (no auto-reload)
|
||||||
|
fileWatcherType = "none"
|
||||||
|
|
||||||
|
# Don’t auto-rerun on file save in production
|
||||||
|
runOnSave = false
|
||||||
|
|
||||||
|
# Better network performance for many clients
|
||||||
|
enableWebsocketCompression = true
|
||||||
|
|
||||||
|
# Optional: if serving behind a subpath (e.g., https://host/sgu)
|
||||||
|
# baseUrlPath = "sgu"
|
||||||
|
|
||||||
|
[browser]
|
||||||
|
# Disable Streamlit telemetry in production
|
||||||
|
gatherUsageStats = false
|
||||||
|
|
||||||
|
[client]
|
||||||
|
# Hide developer toolbar in production
|
||||||
|
toolbarMode = "viewer"
|
||||||
|
|
||||||
|
[logger]
|
||||||
|
# Use info or warning for less noise
|
||||||
|
level = "info"
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
# For production, set a strong cookie secret via environment variable:
|
||||||
|
# STREAMLIT_SERVER_COOKIE_SECRET="<random-32-bytes-hex>"
|
||||||
|
# Don’t store secrets in this file.
|
||||||
7
.streamlit/secrets_template.toml
Normal file
7
.streamlit/secrets_template.toml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Credenciales de PostgreSQL
|
||||||
|
[postgres]
|
||||||
|
host = "host"
|
||||||
|
port = "5432"
|
||||||
|
database = "database"
|
||||||
|
user = "usr"
|
||||||
|
password = "P4ssw0rd."
|
||||||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
WORKDIR /code
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
curl \
|
||||||
|
software-properties-common \
|
||||||
|
git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN mkdir -p /code/.streamlit
|
||||||
|
|
||||||
|
RUN pip install --upgrade pip
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
EXPOSE 8501
|
||||||
|
|
||||||
|
ENTRYPOINT ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0", "--server.runOnSave=true"]
|
||||||
14
README.md
Normal file
14
README.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
## Instalación (Windows PowerShell)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Activar el entorno
|
||||||
|
.\.venv\Scripts\Activate.ps1 # powershell
|
||||||
|
source .venv/Scripts/activate # git bash
|
||||||
|
call venv\Scripts\activate.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ejecución
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
streamlit run app.py
|
||||||
|
```
|
||||||
413
app.py
Normal file
413
app.py
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
import streamlit as st
|
||||||
|
import pandas as pd
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
import json
|
||||||
|
import altair as alt
|
||||||
|
|
||||||
|
st.set_page_config(layout="wide")
|
||||||
|
st.logo("./assets/logo_lasalle.png")
|
||||||
|
SPACE_BETWEEN_SECTIONS = 45
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# LIMPIEZA DE DATAFRAMES
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def _limpia_info(df):
|
||||||
|
# Limpieza de columnas
|
||||||
|
df["promedio"] = pd.to_numeric(df["promedio"].replace("", 0).fillna(0), errors="coerce").fillna(0)
|
||||||
|
df["creditos_totales"] = pd.to_numeric(df["creditos_totales"].replace("", 0).fillna(0), errors="coerce").astype(int)
|
||||||
|
df["servicio_social"] = df["servicio_social"].fillna(False)
|
||||||
|
df["calificacion"] = (
|
||||||
|
df["calificacion"]
|
||||||
|
.replace({"SD": 0, "NP": 0})
|
||||||
|
.fillna(0)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _iniciales_materia(materia_desc):
|
||||||
|
palabras = materia_desc.split()
|
||||||
|
iniciales = ''.join([palabra[0].upper() for palabra in palabras if palabra])
|
||||||
|
return iniciales
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# CARGA Y PROCESAMIENTO DE DATOS DESDE POSTGRESQL
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
@st.cache_data(show_spinner="Cargando información de la base de datos, esto puede tardar unos segundos...")
|
||||||
|
def _crea_df(conn_str):
|
||||||
|
|
||||||
|
engine = create_engine(conn_str)
|
||||||
|
|
||||||
|
# --- Carga inicial desde SQL ---
|
||||||
|
df_info_completa = pd.read_sql(
|
||||||
|
'''
|
||||||
|
SELECT al."Usuario_claveULSA" as "claveULSA", al."Alumno_sexo" as sexo, al."Alumno_semestre" as semestre, al."Carrera_id" as carrera_clave, ca."Carrera_prefijo" as carrera_prefijo,
|
||||||
|
"Alumno_serviciosocial" as servicio_social, "Alumno_beca" as beca, "Alumno_promedioSalle" as promedio, "Alumno_creditos" as creditos_totales, mca."Carrera_prefijo" as carrera_mat_prefijo,
|
||||||
|
m."Materia_desc" as materia, m."Materia_id" as materia_clave, p."Periodo_shortname" as periodo, m."Materia_semestre" as materia_semestre,
|
||||||
|
amc."Calificacion_calif" as calificacion, tc."TipoCalificacion_desc_corta" as tipo_calificacion
|
||||||
|
FROM "Alumno_Materia_Calificacion" amc
|
||||||
|
INNER JOIN "Materia" m ON amc."Materia_id" = m."Materia_id"
|
||||||
|
INNER JOIN "Periodo" p ON amc."Periodo_id" = p."Periodo_id"
|
||||||
|
INNER JOIN "TipoCalificacion" tc ON amc."TipoCalificacion_id" = tc."TipoCalificacion_id"
|
||||||
|
INNER JOIN "Alumno_view" al ON amc."Usuario_claveULSA" = al."Usuario_claveULSA"
|
||||||
|
INNER JOIN "Carrera" ca ON al."Carrera_id" = ca."Carrera_id"
|
||||||
|
INNER JOIN "PlanEstudio" mpe ON m."PlanEstudio_id" = mpe."PlanEstudio_id"
|
||||||
|
INNER JOIN "Carrera" mca ON mpe."Carrera_id" = mca."Carrera_id"
|
||||||
|
WHERE al."EstadoAlumno_isActivo" = true and al."Nivel_id" = 1
|
||||||
|
''',
|
||||||
|
engine
|
||||||
|
)
|
||||||
|
|
||||||
|
# LIMPIAR DATAFRAMES
|
||||||
|
_limpia_info(df_info_completa)
|
||||||
|
|
||||||
|
df_info_profesores = pd.read_sql(
|
||||||
|
'''
|
||||||
|
SELECT * from fs_profesorespromedio(NULL, 1, NULL);
|
||||||
|
''',
|
||||||
|
engine
|
||||||
|
)
|
||||||
|
df_info_profesores.rename(columns={
|
||||||
|
"Usuario_claveULSA": "claveULSA",
|
||||||
|
"Carrera_prefijo": "carrera_prefijo",
|
||||||
|
"Periodo_shortname": "periodo",
|
||||||
|
"promedio_final": "promedio",
|
||||||
|
}, inplace=True)
|
||||||
|
#df_info_profesores["profesor_nombre"] = df_info_profesores["Usuario_nombre"]+" "+df_info_profesores["Usuario_apellidos"]
|
||||||
|
|
||||||
|
return df_info_completa, df_info_profesores
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# FUNCIÓN INICIAL (USA CACHE)
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
def _init_datos():
|
||||||
|
secrets = st.secrets["postgres"]
|
||||||
|
|
||||||
|
conn_str = (
|
||||||
|
f"postgresql+psycopg2://{secrets['user']}:{secrets['password']}"
|
||||||
|
f"@{secrets['host']}:{secrets['port']}/{secrets['database']}"
|
||||||
|
)
|
||||||
|
df_info_completa, df_info_profesores = _crea_df(conn_str)
|
||||||
|
return df_info_completa, df_info_profesores
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# DASHBOARD
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
st.title("📊 Dashboard de alumnos de Ingeniería")
|
||||||
|
|
||||||
|
df_info_completa, df_info_profesores = _init_datos()
|
||||||
|
|
||||||
|
tabInfo, tabMaterias, tabAlumnos, tabProfesores = st.tabs([ "❓Acerca de", "📒 Análisis por Materias", "🚻 Análisis por Alumnos", "🧑🎓 Análisis por Profesores"])
|
||||||
|
with tabInfo:
|
||||||
|
st.write("Los datos presentados en este dashboard corresponden a los alumnos de Ingeniería de la Universidad La Salle, con información obtenida en la extracción de datos.")
|
||||||
|
st.write("El objetivo es proporcionar una visión general del desempeño académico, distribución de becas, servicio social y otros aspectos relevantes de los estudiantes.")
|
||||||
|
st.write("Los datos se han obtenido directamente de la base de datos institucional y se han limpiado para asegurar su precisión y utilidad en el análisis. En las siguientes secciones podrás explorar diferentes perspectivas sobre el rendimiento académico tanto a nivel de materias, alumnos individuales y profesores.")
|
||||||
|
st.write("Debido a la naturaleza de la extracción y limpieza de datos, es posible que algunos registros no estén completos o que ciertos promedios se calculen solo con la información disponible. Se recomienda interpretar los resultados considerando estas limitaciones.")
|
||||||
|
|
||||||
|
with tabMaterias:
|
||||||
|
# csv_historial = df_historial.to_csv(index=False).encode('utf-8')
|
||||||
|
# st.download_button(
|
||||||
|
# label="Descargar historial completo (CSV)",
|
||||||
|
# data=csv_historial,
|
||||||
|
# file_name='historial.csv',
|
||||||
|
# mime='text/csv',
|
||||||
|
# icon="📥",
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
|
df = df_info_completa.copy()
|
||||||
|
periodos_list = df["periodo"].unique().tolist()
|
||||||
|
periodos_list.sort()
|
||||||
|
tipo_calif_list = df["tipo_calificacion"].unique().tolist()
|
||||||
|
|
||||||
|
col1, col2, col3 = st.columns([2,1,2])
|
||||||
|
with col1:
|
||||||
|
# periodos = st.pills("Selecciona un periodo", periodos_list, help="Los periodos 1 son de Ago-Dic, los 2 de Ene-Jun,los I de Invierno y los V de Verano.", default=periodos_list[-1])
|
||||||
|
periodos = st.select_slider("Selecciona un periodo", periodos_list, help="Los periodos 1 son de Ago-Dic, los 2 de Ene-Jun,los I de Invierno y los V de Verano.", value=periodos_list[-3])
|
||||||
|
with col2:
|
||||||
|
tipo_calif = st.pills("Selecciona el tipo de calificación", tipo_calif_list, help="Tipo de calificación: Ordinaria, Extraordinaria, etc.", selection_mode="multi", default=tipo_calif_list)
|
||||||
|
with col3:
|
||||||
|
carrera_mat_list = df_info_completa["carrera_mat_prefijo"].unique().tolist()
|
||||||
|
carrera_mat_list.sort()
|
||||||
|
carrera_mat = st.pills("Selecciona la carrera", carrera_mat_list, help="Carrera del alumno.", selection_mode="multi", default=carrera_mat_list, key="carrera_mat")
|
||||||
|
|
||||||
|
|
||||||
|
if len(tipo_calif) > 0:
|
||||||
|
#df_periodo = df[df["periodo"] == periodos]
|
||||||
|
df_periodo = df[(df["periodo"] == periodos) & (df["tipo_calificacion"].isin(tipo_calif))]
|
||||||
|
df_periodo_filtrado = df_periodo[df_periodo["carrera_mat_prefijo"].isin(carrera_mat)]
|
||||||
|
alumnos_periodo = df_periodo_filtrado["claveULSA"].nunique()
|
||||||
|
|
||||||
|
df_prom= df_periodo_filtrado.groupby(["semestre"]).mean({"calificacion": "mean", }).reset_index()
|
||||||
|
# st.write(df_periodo.head())
|
||||||
|
|
||||||
|
if df_prom.empty:
|
||||||
|
st.info("No hay datos para los filtros seleccionados.")
|
||||||
|
else:
|
||||||
|
st.space(SPACE_BETWEEN_SECTIONS)
|
||||||
|
row = st.container(horizontal=True)
|
||||||
|
with row:
|
||||||
|
st.metric("Número de materias", f"{df_periodo_filtrado.shape[0]:,}/{df_periodo.shape[0]:,}", border=True, help="Número de materias registradas en el periodo filtrado respecto al total de materias en ese periodo.")
|
||||||
|
st.metric("Promedio de calificación en periodo", f"{df_prom['calificacion'].mean():.2f}", border=True)
|
||||||
|
st.metric("Materias reprobadas en periodo", f"{df_periodo_filtrado[df_periodo_filtrado['calificacion'] < 6].shape[0]} ({df_periodo_filtrado[df_periodo_filtrado['calificacion'] < 6].shape[0] / df_periodo_filtrado.shape[0] * 100:.2f}%)", border=True)
|
||||||
|
st.metric("Alumnos registrados en periodo", f"{alumnos_periodo:,}", border=True, help="Número de alumnos con calificaciones registradas en el periodo filtrado.")
|
||||||
|
|
||||||
|
|
||||||
|
st.space(SPACE_BETWEEN_SECTIONS)
|
||||||
|
col1, col2 = st.columns(2)
|
||||||
|
with col1:
|
||||||
|
st.subheader(f"Promedios del periodo")
|
||||||
|
st.bar_chart(data=df_prom, x="semestre", y="calificacion")
|
||||||
|
|
||||||
|
st.subheader(f"Promedios del periodo agrupado por sexo")
|
||||||
|
df_sexo = (
|
||||||
|
df_periodo_filtrado
|
||||||
|
.groupby(["semestre", "sexo"])["calificacion"]
|
||||||
|
.mean()
|
||||||
|
.reset_index()
|
||||||
|
)
|
||||||
|
chart = (
|
||||||
|
alt.Chart(df_sexo)
|
||||||
|
.mark_line(point=True) # 👈 activa puntos para ver cada valor
|
||||||
|
.encode(
|
||||||
|
x=alt.X("semestre:O", title="Semestre"),
|
||||||
|
y=alt.Y("calificacion:Q", title="Calificación promedio"),
|
||||||
|
color=alt.Color("sexo:N", title="Sexo"),
|
||||||
|
tooltip=["semestre", "sexo", "calificacion"]
|
||||||
|
)
|
||||||
|
.properties(height=400)
|
||||||
|
)
|
||||||
|
|
||||||
|
st.altair_chart(chart, width='stretch')
|
||||||
|
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
st.subheader(f"Frecuencia de calificaciones por materia")
|
||||||
|
df_materias = df_periodo_filtrado.groupby(["calificacion"]).count().reset_index()
|
||||||
|
df_materias = df_materias.sort_values(by="calificacion", ascending=False)
|
||||||
|
df_materias["total"] = df_materias["materia_clave"]
|
||||||
|
st.bar_chart(data=df_materias, x="calificacion", y="total")
|
||||||
|
|
||||||
|
|
||||||
|
min_alumnos_materia = st.slider("Mínimo de alumnos para análisis de reprobación", min_value=1, max_value=40, value=10, help="Número mínimo de alumnos inscritos en una materia para que sea considerada en el análisis de reprobación.")
|
||||||
|
st.subheader(f"Materias con mayor reprobación")
|
||||||
|
# Crear columna con el nombre corto (iniciales + carrera)
|
||||||
|
df_base_reprobacion = df_periodo_filtrado.copy()
|
||||||
|
df_base_reprobacion["materia_corta"] = (
|
||||||
|
df_base_reprobacion["materia"].apply(_iniciales_materia)
|
||||||
|
+ " [" + df_base_reprobacion["carrera_mat_prefijo"] + "]"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Totales
|
||||||
|
df_reprobadas = df_base_reprobacion[df_base_reprobacion["calificacion"] < 6].groupby(["materia_corta", "materia"]).size().reset_index(name="total_reprobadas")
|
||||||
|
df_total = df_base_reprobacion.groupby(["materia_corta", "materia"]).size().reset_index(name="total_registradas")
|
||||||
|
# Unir ambas
|
||||||
|
df_merge = df_total.merge(df_reprobadas, on=["materia_corta", "materia"], how="left")
|
||||||
|
# Llenar NaN en materias sin reprobados
|
||||||
|
df_merge["total_reprobadas"] = df_merge["total_reprobadas"].fillna(0)
|
||||||
|
|
||||||
|
# Crear porcentaje
|
||||||
|
df_merge["porcentaje_reprobacion"] = (
|
||||||
|
df_merge["total_reprobadas"] / df_merge["total_registradas"] * 100
|
||||||
|
)
|
||||||
|
|
||||||
|
df_merge = df_merge[(df_merge["total_registradas"] >= min_alumnos_materia) & (df_merge["porcentaje_reprobacion"] > 0) ]
|
||||||
|
|
||||||
|
# Top 10 según % reprobación
|
||||||
|
df_merge = df_merge.sort_values("porcentaje_reprobacion", ascending=False).head( 10)
|
||||||
|
|
||||||
|
chart = (
|
||||||
|
alt.Chart(df_merge)
|
||||||
|
.mark_bar()
|
||||||
|
.encode(
|
||||||
|
y=alt.Y("materia_corta:N", sort='-x', title="Materia"),
|
||||||
|
x=alt.X("porcentaje_reprobacion:Q", title="Porcentaje de reprobación (%)"),
|
||||||
|
tooltip=[
|
||||||
|
"materia:N",
|
||||||
|
"total_registradas:Q",
|
||||||
|
"total_reprobadas:Q",
|
||||||
|
alt.Tooltip("porcentaje_reprobacion:Q", format=".2f")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
.properties(height=400)
|
||||||
|
)
|
||||||
|
|
||||||
|
st.altair_chart(chart, width='stretch')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# with st.expander("Ver datos filtrados"):
|
||||||
|
# st.dataframe(df_prom)
|
||||||
|
else:
|
||||||
|
st.warning("Por favor selecciona al menos un tipo de calificación para mostrar los datos.")
|
||||||
|
|
||||||
|
with tabAlumnos:
|
||||||
|
# csv_info = df_info_completa.to_csv(index=False).encode('utf-8')
|
||||||
|
# st.download_button(
|
||||||
|
# label="Descargar datos completos (CSV)",
|
||||||
|
# data=csv_info,
|
||||||
|
# file_name='info_completa.csv',
|
||||||
|
# mime='text/csv',
|
||||||
|
# icon="📥",
|
||||||
|
# )
|
||||||
|
# cambiar beca a numérico
|
||||||
|
df_alumnos = df_info_completa.copy()
|
||||||
|
df_alumnos = df_alumnos.drop_duplicates(subset=["claveULSA"])
|
||||||
|
total_alumnos = df_alumnos.shape[0]
|
||||||
|
df_alumnos["beca"] = (df_info_completa["beca"].replace("No tiene beca", "0"))
|
||||||
|
df_alumnos["beca"] = df_alumnos["beca"].fillna("0")
|
||||||
|
df_alumnos["beca"] = pd.to_numeric(df_alumnos["beca"].astype(str).str.replace("%", ""), errors="coerce").fillna(0)
|
||||||
|
|
||||||
|
col1, col2, col3 = st.columns([1,2,2])
|
||||||
|
with col1:
|
||||||
|
genero_list = df_alumnos["sexo"].unique().tolist()
|
||||||
|
genero = st.pills("Selecciona el género", genero_list, help="Género del alumno.", selection_mode="multi", default=genero_list)
|
||||||
|
with col2:
|
||||||
|
semestre_list = df_alumnos["semestre"].unique().tolist()
|
||||||
|
semestre_list.sort()
|
||||||
|
semestre = st.pills("Selecciona el semestre", semestre_list, help="Semestre actual del alumno.", selection_mode="multi", default=semestre_list)
|
||||||
|
with col3:
|
||||||
|
carrera_list = df_info_completa["carrera_prefijo"].unique().tolist()
|
||||||
|
carrera_list.sort()
|
||||||
|
carrera = st.pills("Selecciona la carrera", carrera_list, help="Carrera del alumno.", selection_mode="multi", default=carrera_list)
|
||||||
|
|
||||||
|
|
||||||
|
if len(genero) > 0 and len(semestre) > 0 and len(carrera) > 0:
|
||||||
|
|
||||||
|
df_info_filtrado = df_alumnos.copy()
|
||||||
|
df_info_filtrado = df_info_filtrado[
|
||||||
|
(df_info_filtrado["sexo"].isin(genero)) &
|
||||||
|
(df_info_filtrado["semestre"].isin(semestre)) &
|
||||||
|
(df_info_filtrado["carrera_prefijo"].isin(carrera))
|
||||||
|
]
|
||||||
|
servicio_social_count = df_info_filtrado[df_info_filtrado["servicio_social"] == True].shape[0]
|
||||||
|
servicio_social_perc = (servicio_social_count / df_info_filtrado.shape[0]) * 100 if df_info_filtrado.shape[0] > 0 else 0
|
||||||
|
beca_perc = (df_info_filtrado['beca'] > 0).sum()/df_info_filtrado.shape[0]*100 if df_info_filtrado.shape[0] > 0 else 0
|
||||||
|
promedio_alumnos = df_info_filtrado[df_info_filtrado["promedio"] > 0]["promedio"].mean()
|
||||||
|
|
||||||
|
|
||||||
|
st.space(SPACE_BETWEEN_SECTIONS)
|
||||||
|
row2 = st.container(horizontal=True)
|
||||||
|
with row2:
|
||||||
|
st.metric("Total de alumnos", f"{df_info_filtrado.shape[0]:,} ({df_info_filtrado.shape[0]/total_alumnos*100:.2f}%)", border=True, help="Número total y porcentaje de alumnos filtrados respecto al total de alumnos.")
|
||||||
|
st.metric("Alumnos con beca", f"{(df_info_filtrado['beca'] > 0).sum():,} ({beca_perc:.2f}%)", border=True, help="Número y porcentaje de alumnos que cuentan con algún tipo de beca.")
|
||||||
|
st.metric("Creditos promedio", f"{df_info_filtrado['creditos_totales'].mean():.2f}", border=True, help="Pomedio de créditos totales de formación")
|
||||||
|
st.metric("Alumnos con servicio social", f"{servicio_social_count:,} ({servicio_social_perc:.2f}%)", border=True)
|
||||||
|
st.metric("Promedio general", f"{promedio_alumnos:.2f}", border=True, help="Promedio general de los alumnos filtrados. No cuenta de nuevo ingreso")
|
||||||
|
|
||||||
|
st.space(SPACE_BETWEEN_SECTIONS)
|
||||||
|
|
||||||
|
col1, col2 = st.columns(2)
|
||||||
|
with col1:
|
||||||
|
st.subheader("Distribución de becas")
|
||||||
|
beca_counts = df_info_filtrado["beca"].value_counts().reset_index()
|
||||||
|
beca_counts.columns = ["Beca (%)", "Total"]
|
||||||
|
st.bar_chart(data=beca_counts, x="Beca (%)", y="Total")
|
||||||
|
with col2:
|
||||||
|
st.subheader("Promedio de alumnos por carrera")
|
||||||
|
carrera_prom = df_info_filtrado[df_info_filtrado["promedio"] > 0]
|
||||||
|
carrera_prom = carrera_prom.groupby("carrera_prefijo").mean({"promedio": "mean"}).reset_index()
|
||||||
|
st.bar_chart(data=carrera_prom, x="carrera_prefijo", y="promedio")
|
||||||
|
|
||||||
|
# st.write(f"Total de alumnos filtrados: {df_info_filtrado.shape[0]}")
|
||||||
|
#alumnos por carrera
|
||||||
|
else:
|
||||||
|
st.warning("Por favor selecciona al menos un valor en cada filtro para mostrar los datos.")
|
||||||
|
|
||||||
|
|
||||||
|
with tabProfesores:
|
||||||
|
df_prof = df_info_profesores.copy()
|
||||||
|
|
||||||
|
col1, col2 = st.columns(2)
|
||||||
|
with col1:
|
||||||
|
# periodos = st.pills("Selecciona un periodo", periodos_list, help="Los periodos 1 son de Ago-Dic, los 2 de Ene-Jun,los I de Invierno y los V de Verano.", default=periodos_list[-1])
|
||||||
|
periodos_prof = st.select_slider("Selecciona un periodo", periodos_list, help="Los periodos 1 son de Ago-Dic, los 2 de Ene-Jun,los I de Invierno y los V de Verano.", value=periodos_list[-3], key="periodo_prof")
|
||||||
|
with col2:
|
||||||
|
carrera_prof = st.pills("Selecciona la carrera", carrera_mat_list, help="Carrera en la que imparte el profesor.", selection_mode="multi", default=carrera_mat_list, key="carrera_prof")
|
||||||
|
|
||||||
|
df_prof_periodo = df_prof[(df_prof["periodo"] == periodos_prof) & (df_prof["carrera_prefijo"].isin(carrera_prof))]
|
||||||
|
|
||||||
|
st.subheader("Buscar profesor")
|
||||||
|
col1, col2, col3 = st.columns([1,1,2])
|
||||||
|
with col1:
|
||||||
|
profesor_buscar = st.selectbox("Selecciona un profesor", df_prof_periodo["profesor_nombre"].unique().tolist(), key="profesor_buscar")
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
# 1. Calcular el promedio por profesor
|
||||||
|
df_rank = (df_prof_periodo.groupby("profesor_nombre", as_index=False).agg(promedio=("promedio", "mean")))
|
||||||
|
|
||||||
|
# 2. Ordenar por promedio (de mayor a menor) y resetear índice
|
||||||
|
df_rank = df_rank.sort_values(by="promedio",ascending=False).reset_index(drop=True)
|
||||||
|
|
||||||
|
# 3. Agregar ranking comenzando en 1
|
||||||
|
df_rank["ranking"] = df_rank.index + 1
|
||||||
|
|
||||||
|
# 4. Filtrar el profesor seleccionado
|
||||||
|
fila_prof = df_rank[df_rank["profesor_nombre"] == profesor_buscar]
|
||||||
|
|
||||||
|
if fila_prof.empty:
|
||||||
|
st.warning("El profesor seleccionado no tiene registros en este periodo.")
|
||||||
|
else:
|
||||||
|
ranking_profesor = int(fila_prof["ranking"].iloc[0])
|
||||||
|
promedio_profesor = float(fila_prof["promedio"].iloc[0])
|
||||||
|
st.metric("Promedio del profesor", f"{promedio_profesor:.2f}", border=True, delta=f"{ranking_profesor}/{df_rank.shape[0]}", help="Promedio de calificaciones del profesor en el periodo seleccionado.")
|
||||||
|
|
||||||
|
with col3:
|
||||||
|
st.info("No se consideran calificaciones de 0 (SD, NP) en los promedios de los profesores.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
col1, col2 = st.columns(2)
|
||||||
|
with col1:
|
||||||
|
st.subheader("Promedios más altos por profesor")
|
||||||
|
#df_prof_periodo_top = df_prof_periodo.groupby("profesor_nombre").mean({"promedio": "mean"}).reset_index()
|
||||||
|
df_prof_periodo_top = df_prof_periodo.groupby("profesor_nombre").agg(
|
||||||
|
promedio=("promedio", "mean"),
|
||||||
|
total_alumnos=("total_alumnos", "sum") # O "count" si viene repetido por alumno
|
||||||
|
).reset_index()
|
||||||
|
df_prof_periodo_top = df_prof_periodo_top.sort_values(by="promedio", ascending=False).head(10)
|
||||||
|
chart = (
|
||||||
|
alt.Chart(df_prof_periodo_top)
|
||||||
|
.mark_bar()
|
||||||
|
.encode(
|
||||||
|
y=alt.Y("profesor_nombre:N", sort='-x', title="Profesores", axis=alt.Axis(labelLimit=300)),
|
||||||
|
x=alt.X("promedio:Q", title="Promedio de calificaciones"),
|
||||||
|
tooltip=[
|
||||||
|
"profesor_nombre:N",
|
||||||
|
"promedio:Q",
|
||||||
|
"total_alumnos:Q"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
.properties(height=400)
|
||||||
|
)
|
||||||
|
|
||||||
|
st.altair_chart(chart, width='stretch')
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
st.subheader("Promedios más bajos por profesor")
|
||||||
|
#df_prof_periodo_top = df_prof_periodo.groupby("profesor_nombre").mean({"promedio": "mean"}).reset_index()
|
||||||
|
df_prof_periodo_top = df_prof_periodo.groupby("profesor_nombre").agg(
|
||||||
|
promedio=("promedio", "mean"),
|
||||||
|
total_alumnos=("total_alumnos", "sum") # O "count" si viene repetido por alumno
|
||||||
|
).reset_index()
|
||||||
|
|
||||||
|
df_prof_periodo_top = df_prof_periodo_top.sort_values(by="promedio", ascending=True).head(10)
|
||||||
|
chart = (
|
||||||
|
alt.Chart(df_prof_periodo_top)
|
||||||
|
.mark_bar()
|
||||||
|
.encode(
|
||||||
|
y=alt.Y("profesor_nombre:N", sort='x', title="Profesores", axis=alt.Axis(labelLimit=300)),
|
||||||
|
x=alt.X("promedio:Q", title="Promedio de calificaciones"),
|
||||||
|
tooltip=[
|
||||||
|
"profesor_nombre:N",
|
||||||
|
"promedio:Q",
|
||||||
|
"total_alumnos:Q"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
.properties(height=400)
|
||||||
|
)
|
||||||
|
|
||||||
|
st.altair_chart(chart, width='stretch')
|
||||||
BIN
assets/logo_lasalle.png
Normal file
BIN
assets/logo_lasalle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
streamlit
|
||||||
|
pandas
|
||||||
|
numpy
|
||||||
|
matplotlib
|
||||||
|
seaborn
|
||||||
|
sqlalchemy
|
||||||
|
psycopg2-binary
|
||||||
Reference in New Issue
Block a user