ChangesApp / src /streamlit_app.py
Migue1804's picture
Update src/streamlit_app.py
006ed49 verified
import streamlit as st
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Ellipse
from scipy.interpolate import make_interp_spline
import numpy as np
import pandas as pd
# Datos de satisfacción por edad para cada región
region_data = {
"United States": [7.0, 6.5, 6.2, 6.0, 6.3, 6.8, 7.3, 7.8, 8.1],
"United Kingdom": [6.8, 6.3, 6.0, 6.1, 6.4, 6.7, 7.2, 7.6, 8.0],
"Latin America and Caribbean": [6.5, 6.1, 6.0, 6.2, 6.4, 6.6, 6.9, 7.2, 7.5],
"China": [5.5, 5.0, 4.8, 4.7, 5.0, 5.5, 6.0, 6.5, 7.0],
"Germany": [6.8, 6.2, 5.8, 5.7, 6.0, 6.3, 6.6, 6.9, 7.2],
"Russia": [5.8, 5.4, 5.1, 4.8, 4.6, 4.8, 5.0, 5.2, 5.5],
"Mundo": [5.58, 5.47, 5.38, 5.32, 5.26, 5.22, 5.20, 5.22, 5.28, 5.36, 5.45, 5.55, 5.65, 5.75],
"Países más felices": [7.4, 6.9, 6.6, 6.5, 6.6, 6.9, 7.3, 7.8, 8.2],
"Países menos felices": [6.8, 6.3, 6.0, 5.9, 6.0, 6.3, 6.7, 7.2, 7.6],
"Países menos felices (extremo)": [6.0, 5.2, 4.8, 4.5, 4.6, 5.0, 5.6, 6.2, 6.7]
}
# Datos PISA 2022
pisa_data = {
"Singapore": {"Math": 575, "Reading": 543, "Science": 561},
"Macao (China)": {"Math": 552, "Reading": 510, "Science": 543},
"Chinese Taipei": {"Math": None, "Reading": 515, "Science": 537},
"Hong Kong (China)": {"Math": 540, "Reading": 500, "Science": 520},
"Japan": {"Math": 536, "Reading": 516, "Science": 547},
"Korea": {"Math": 527, "Reading": 515, "Science": 528},
"Estonia": {"Math": 510, "Reading": 511, "Science": 526},
"Switzerland": {"Math": 508, "Reading": 483, "Science": 503},
"Canada": {"Math": 497, "Reading": 507, "Science": 515},
"Netherlands": {"Math": 493, "Reading": 459, "Science": 488},
"Ireland": {"Math": 492, "Reading": 516, "Science": 504},
"Belgium": {"Math": 489, "Reading": 479, "Science": 491},
"Denmark": {"Math": 489, "Reading": 489, "Science": 494},
"United Kingdom": {"Math": 489, "Reading": 494, "Science": 500},
"Poland": {"Math": 489, "Reading": 489, "Science": 499},
"Austria": {"Math": 487, "Reading": 480, "Science": 491},
"Australia": {"Math": 487, "Reading": 498, "Science": 507},
"Czech Republic": {"Math": 487, "Reading": 489, "Science": 498},
"Slovenia": {"Math": 485, "Reading": 469, "Science": 500},
"Finland": {"Math": 484, "Reading": 490, "Science": 511},
"Latvia": {"Math": 483, "Reading": 475, "Science": 494},
"Sweden": {"Math": 482, "Reading": 487, "Science": 494},
"New Zealand": {"Math": 479, "Reading": 501, "Science": 504},
"Lithuania": {"Math": 475, "Reading": 472, "Science": 484},
"Germany": {"Math": 475, "Reading": 480, "Science": 492},
"France": {"Math": 474, "Reading": 474, "Science": 487},
"Spain": {"Math": 473, "Reading": 474, "Science": 485},
"Hungary": {"Math": 473, "Reading": 473, "Science": 486},
"Portugal": {"Math": 472, "Reading": 477, "Science": 484},
"Italy": {"Math": 471, "Reading": 482, "Science": 477},
"Norway": {"Math": 468, "Reading": 477, "Science": 478},
"United States": {"Math": 465, "Reading": 504, "Science": 499},
"Slovak Republic": {"Math": 464, "Reading": 447, "Science": 462},
"Croatia": {"Math": 463, "Reading": 475, "Science": 483},
"Iceland": {"Math": 459, "Reading": 436, "Science": 447},
"Israel": {"Math": 458, "Reading": 474, "Science": 465},
"Turkey": {"Math": 453, "Reading": 456, "Science": 476},
"Ukraine": {"Math": 441, "Reading": 428, "Science": 450},
"Serbia": {"Math": 440, "Reading": 440, "Science": 447},
"United Arab Emirates": {"Math": 431, "Reading": 417, "Science": 432},
"Greece": {"Math": 430, "Reading": 438, "Science": 441},
"Romania": {"Math": 428, "Reading": 428, "Science": 428},
"Kazakhstan": {"Math": 425, "Reading": 386, "Science": 423},
"Mongolia": {"Math": 425, "Reading": 378, "Science": 412},
"Bulgaria": {"Math": 417, "Reading": 404, "Science": 421},
"Moldova": {"Math": 414, "Reading": 411, "Science": 417},
"Qatar": {"Math": 414, "Reading": 419, "Science": 432},
"Chile": {"Math": 412, "Reading": 448, "Science": 444},
"Uruguay": {"Math": 409, "Reading": 430, "Science": 435},
"Malaysia": {"Math": 409, "Reading": 388, "Science": 416},
"Montenegro": {"Math": 406, "Reading": 405, "Science": 403},
"Mexico": {"Math": 395, "Reading": 415, "Science": 410},
"Thailand": {"Math": 394, "Reading": 379, "Science": 409},
"Peru": {"Math": 391, "Reading": 408, "Science": 408},
"Georgia": {"Math": 390, "Reading": 374, "Science": 384},
"Saudi Arabia": {"Math": 389, "Reading": 383, "Science": 390},
"North Macedonia": {"Math": 389, "Reading": 359, "Science": 380},
"Costa Rica": {"Math": 385, "Reading": 415, "Science": 411},
"Colombia": {"Math": 383, "Reading": 409, "Science": 411},
"Brazil": {"Math": 379, "Reading": 410, "Science": 403},
"Argentina": {"Math": 378, "Reading": 401, "Science": 406},
"Jamaica": {"Math": 377, "Reading": 410, "Science": 403},
"Albania": {"Math": 368, "Reading": 358, "Science": 376},
"Palestine": {"Math": 366, "Reading": 349, "Science": 369},
"Indonesia": {"Math": 366, "Reading": 359, "Science": 383},
"Morocco": {"Math": 365, "Reading": 339, "Science": 365},
"Uzbekistan": {"Math": 364, "Reading": 336, "Science": 355},
"Jordan": {"Math": 361, "Reading": 342, "Science": 375},
"Panama": {"Math": 357, "Reading": 392, "Science": 388},
"Kosovo": {"Math": 355, "Reading": 342, "Science": 357},
"Philippines": {"Math": 355, "Reading": 347, "Science": 356},
"Guatemala": {"Math": 344, "Reading": 374, "Science": 373},
"El Salvador": {"Math": 343, "Reading": 365, "Science": 373},
"Dominican Republic": {"Math": 339, "Reading": 351, "Science": 360},
"Paraguay": {"Math": 338, "Reading": 373, "Science": 368},
"Cambodia": {"Math": 336, "Reading": 329, "Science": 347}
}
# Edades diferentes para los datos del mundo
world_ages = [20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85]
ages = [16, 26, 36, 46, 56, 66, 76, 86, 96]
# Streamlit App
st.set_page_config(page_title="ïndices de cambio", layout="wide")
st.title("📊 Análisis de curvas de cambios predecibles, ¿o no?")
# Tabs
tab1, tab2 = st.tabs(["Indice de felicidad", "🎓 Indice educativo"])
with tab1:
st.header("La curva de la vida como una sonrisa")
col1, col2 = st.columns([1, 2])
with col1:
# Selectbox para región
region = st.selectbox("Selecciona la región:", list(region_data.keys()))
# Determinar rango de edades según la región
if region == "Mundo":
min_age, max_age = 20, 85
current_ages = world_ages
else:
min_age, max_age = 16, 96
current_ages = ages
# Slider para la edad
edad = st.slider("Selecciona tu edad:",
min_value=min_age,
max_value=max_age,
step=1)
with col2:
satisfaction = region_data[region]
# Normalizar los datos
if region == "Mundo":
x_norm = np.array([(age - 52.5) / 32.5 for age in current_ages])
y_norm = np.array([(s - 5.4) / 2.5 - 0.3 for s in satisfaction])
edad_interp = (edad - 52.5) / 32.5
else:
x_norm = np.array([(age - 56) / 50 for age in current_ages])
y_norm = np.array([(s - 6.5) / 4 - 0.3 for s in satisfaction])
edad_interp = (edad - 56) / 50
# Suavizar la curva
x_smooth = np.linspace(x_norm.min(), x_norm.max(), 300)
spl = make_interp_spline(x_norm, y_norm, k=3)
y_smooth = spl(x_smooth)
# Encontrar posición de la edad seleccionada
satisf_interp = float(spl(edad_interp)) if x_norm.min() <= edad_interp <= x_norm.max() else None
# Crear gráfica
fig, ax = plt.subplots(figsize=(8, 8))
ax.set_aspect('equal')
ax.axis('off')
# Cara
cara = Circle((0, 0), 1, facecolor='gold', edgecolor='orange', linewidth=4)
ax.add_patch(cara)
# Sonrisa
ax.plot(x_smooth, y_smooth, color="black", linewidth=3)
# Ojos
ax.plot(-0.3, 0.3, 'o', markersize=15, color='black')
ax.plot(0.3, 0.3, 'o', markersize=15, color='black')
# Lengua para mostrar la edad seleccionada
if satisf_interp is not None:
tongue_main = Ellipse((edad_interp, satisf_interp - 0.06), width=0.18, height=0.12,
facecolor='#ff4444', edgecolor='#cc0000', linewidth=2)
ax.add_patch(tongue_main)
tongue_top = Ellipse((edad_interp, satisf_interp - 0.04), width=0.14, height=0.08,
facecolor='#ff6666', alpha=0.8)
ax.add_patch(tongue_top)
ax.plot([edad_interp, edad_interp],
[satisf_interp - 0.02, satisf_interp - 0.10],
color='#aa0000', linewidth=1, alpha=0.7)
ax.text(edad_interp, satisf_interp - 0.22, f"{edad} años",
color='white', fontsize=10, ha='center', weight='bold',
bbox=dict(boxstyle="round,pad=0.3", facecolor='red',
edgecolor='darkred', linewidth=2))
ax.set_xlim(-1.2, 1.2)
ax.set_ylim(-1.2, 1.2)
st.pyplot(fig)
# Información adicional
if satisf_interp is not None:
if region == "Mundo":
satisfaccion_real = (satisf_interp + 0.3) * 2.5 + 5.4
else:
satisfaccion_real = (satisf_interp + 0.3) * 4 + 6.5
st.info(f"A los {edad} años en {region}, el nivel de satisfacción promedio es: {satisfaccion_real:.2f}/10")
# Comparación entre regiones
st.subheader("Comparación entre regiones")
fig2, ax2 = plt.subplots(figsize=(14, 8))
countries_data = {k: v for k, v in region_data.items() if k not in ["Mundo"]}
for region_name, values in countries_data.items():
current_ages_plot = world_ages if region_name == "Mundo" else ages
ax2.plot(current_ages_plot, values, marker='o', label=region_name, linewidth=2, markersize=6)
ax2.plot(world_ages, region_data["Mundo"], marker='s', label="Mundo",
linewidth=3, markersize=8, color='black', linestyle='--')
ax2.set_xlabel('Edad (años)', fontsize=12)
ax2.set_ylabel('Nivel de Satisfacción', fontsize=12)
ax2.set_title('Curva de Satisfacción por Edad - Comparación Global', fontsize=14, fontweight='bold')
ax2.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
st.pyplot(fig2)
with tab2:
st.header("Indices educativos con base en PISA 2022")
# Preparar datos para los histogramas
subjects = ["Math", "Reading", "Science"]
subject_names = {"Math": "Matemáticas", "Reading": "Lectura", "Science": "Ciencias"}
col1, col2 = st.columns([1, 3])
with col1:
# Selector de materia
selected_subject = st.selectbox("Selecciona la materia:",
["Math", "Reading", "Science"],
format_func=lambda x: subject_names[x])
# Obtener datos válidos para la materia seleccionada
valid_scores = [score[selected_subject] for score in pisa_data.values()
if score[selected_subject] is not None]
if valid_scores:
min_score = min(valid_scores)
max_score = max(valid_scores)
# Opciones de selección
selection_type = st.radio("Seleccionar por:", ["País", "Puntaje"])
if selection_type == "País":
# Lista de países con datos válidos para la materia seleccionada
valid_countries = [country for country, scores in pisa_data.items()
if scores[selected_subject] is not None]
selected_country = st.selectbox("Selecciona un país:", valid_countries)
selected_score = pisa_data[selected_country][selected_subject]
else: # Selección por puntaje
selected_score = st.slider("Selecciona un puntaje:",
min_value=int(min_score),
max_value=int(max_score),
value=int((min_score + max_score) / 2))
# Encontrar el país más cercano a este puntaje
closest_country = min(pisa_data.keys(),
key=lambda x: abs(pisa_data[x][selected_subject] - selected_score)
if pisa_data[x][selected_subject] is not None else float('inf'))
selected_country = closest_country
with col2:
# Crear ranking para identificar top y bottom 3
ranking = [(country, scores[selected_subject]) for country, scores in pisa_data.items()
if scores[selected_subject] is not None]
ranking.sort(key=lambda x: x[1], reverse=True)
# Obtener top 3 y bottom 3
top_3 = ranking[:3]
bottom_3 = ranking[-3:]
# Configurar estilo moderno
plt.style.use('default')
fig, ax = plt.subplots(figsize=(14, 9))
fig.patch.set_facecolor('#f8f9fa')
ax.set_facecolor('#ffffff')
# Crear histograma con mejor diseño
n, bins, patches = ax.hist(valid_scores, bins=25, alpha=0.8,
color='#6c7ae0', edgecolor='white', linewidth=1.5)
# Aplicar gradiente de colores
for i, patch in enumerate(patches):
bin_left = bins[i]
bin_right = bins[i + 1]
# Colorear según si contiene países destacados
contains_top_3 = any(bin_left <= score <= bin_right for _, score in top_3)
contains_bottom_3 = any(bin_left <= score <= bin_right for _, score in bottom_3)
if contains_top_3:
patch.set_facecolor('#FFD700')
patch.set_edgecolor('#FF8C00')
patch.set_linewidth(2)
patch.set_alpha(0.9)
elif contains_bottom_3:
patch.set_facecolor('#FF6B6B')
patch.set_edgecolor('#DC143C')
patch.set_linewidth(2)
patch.set_alpha(0.9)
else:
# Gradiente suave para otras barras
intensity = i / len(patches)
patch.set_facecolor(plt.cm.Blues(0.4 + intensity * 0.4))
patch.set_alpha(0.7)
# Línea vertical para el país seleccionado
ax.axvline(x=selected_score, color='#e74c3c', linestyle='--', linewidth=4,
alpha=0.9, label=f'{selected_country}: {selected_score}')
# Añadir etiquetas para TOP 3 con mejor posicionamiento
for i, (country, score) in enumerate(top_3):
ax.axvline(x=score, color='#f39c12', linestyle='-', alpha=0.8, linewidth=3)
# Calcular posición y para evitar superposición
y_pos = max(n) * (0.95 - i * 0.08)
# Crear etiqueta con fondo
bbox_props = dict(boxstyle="round,pad=0.4", facecolor='#FFD700',
edgecolor='#FF8C00', alpha=0.9, linewidth=2)
ax.annotate(f'🥇 {country}\n{score}' if i == 0 else
f'🥈 {country}\n{score}' if i == 1 else
f'🥉 {country}\n{score}',
xy=(score, 0), xytext=(score, y_pos),
ha='center', va='bottom', fontsize=10, fontweight='bold',
bbox=bbox_props, color='#8B4513',
arrowprops=dict(arrowstyle='->', color='#FF8C00', lw=2))
# Añadir etiquetas para BOTTOM 3 con mejor diseño
for i, (country, score) in enumerate(bottom_3):
ax.axvline(x=score, color='#c0392b', linestyle='-', alpha=0.8, linewidth=3)
# Calcular posición y para la parte inferior
y_pos = max(n) * (0.35 - i * 0.08)
# Crear etiqueta con fondo
bbox_props = dict(boxstyle="round,pad=0.4", facecolor='#FF6B6B',
edgecolor='#DC143C', alpha=0.9, linewidth=2)
rank = len(ranking) - 2 + i
ax.annotate(f'{rank}° {country}\n{score}',
xy=(score, 0), xytext=(score, y_pos),
ha='center', va='bottom', fontsize=10, fontweight='bold',
bbox=bbox_props, color='white',
arrowprops=dict(arrowstyle='->', color='#DC143C', lw=2))
# Línea del promedio con mejor estilo
mean_score = np.mean(valid_scores)
ax.axvline(x=mean_score, color='#27ae60', linestyle=':', linewidth=3,
alpha=0.8, label=f'Promedio Global: {mean_score:.1f}')
# Mejorar el diseño general
ax.set_xlabel('Puntaje PISA', fontsize=14, fontweight='bold', color='#2c3e50')
ax.set_ylabel('Número de Países/Regiones', fontsize=14, fontweight='bold', color='#2c3e50')
ax.set_title(f'🌍 Distribución Global PISA 2022 - {subject_names[selected_subject]}',
fontsize=16, fontweight='bold', color='#2c3e50', pad=20)
# Personalizar grid
ax.grid(True, alpha=0.3, linestyle='-', linewidth=0.5, color='#bdc3c7')
ax.set_axisbelow(True)
# Mejorar bordes y espinas
for spine in ax.spines.values():
spine.set_color('#bdc3c7')
spine.set_linewidth(1)
# Crear leyenda personalizada más elegante
from matplotlib.patches import Patch
legend_elements = [
plt.Line2D([0], [0], color='#e74c3c', linestyle='--', linewidth=4,
label=f'País Seleccionado: {selected_country}'),
plt.Line2D([0], [0], color='#27ae60', linestyle=':', linewidth=3,
label=f'Promedio Global: {mean_score:.1f}'),
Patch(facecolor='#FFD700', edgecolor='#FF8C00', alpha=0.9,
label='🏆 Top 3 Países'),
Patch(facecolor='#FF6B6B', edgecolor='#DC143C', alpha=0.9,
label='📉 Bottom 3 Países'),
Patch(facecolor='#6c7ae0', alpha=0.7, label='Otros Países')
]
legend = ax.legend(handles=legend_elements, loc='upper center',
fontsize=11, frameon=True, fancybox=True,
shadow=True, framealpha=0.9)
legend.get_frame().set_facecolor('#f8f9fa')
legend.get_frame().set_edgecolor('#bdc3c7')
# Ajustar límites para mejor visualización
ax.set_xlim(min_score - 20, max_score + 20)
plt.tight_layout()
st.pyplot(fig)
# Información estadística mejorada
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("🏛️ País Seleccionado", selected_country)
with col2:
st.metric("📊 Puntaje", f"{selected_score}")
with col3:
st.metric("🌍 Promedio Global", f"{np.mean(valid_scores):.1f}")
with col4:
percentile = (sum(1 for score in valid_scores if score < selected_score) / len(valid_scores)) * 100
st.metric("📈 Percentil", f"{percentile:.1f}%")
# Top 10 y Bottom 10
st.subheader(f"🏆 Ranking Mundial en {subject_names[selected_subject]}")
col1, col2 = st.columns(2)
# Crear ranking
ranking = [(country, scores[selected_subject]) for country, scores in pisa_data.items()
if scores[selected_subject] is not None]
ranking.sort(key=lambda x: x[1], reverse=True)
with col1:
st.write("**🏆 Top 10 países:**")
top_10 = ranking[:10]
for i, (country, score) in enumerate(top_10, 1):
emoji = "🥇" if i == 1 else "🥈" if i == 2 else "🥉" if i == 3 else f"{i}."
st.write(f"{emoji} {country}: {score}")
with col2:
st.write("**📉 Bottom 10 países:**")
bottom_10 = ranking[-10:]
bottom_10.reverse()
for i, (country, score) in enumerate(bottom_10, 1):
st.write(f"{len(ranking) - i + 1}. {country}: {score}")
# Comparación por materias
st.subheader("📚 Comparación por Materias")
if selection_type == "País":
country_scores = pisa_data[selected_country]
valid_subjects = [subj for subj in subjects if country_scores[subj] is not None]
if len(valid_subjects) > 1:
fig, ax = plt.subplots(figsize=(12, 7))
fig.patch.set_facecolor('#f8f9fa')
ax.set_facecolor('#ffffff')
subject_scores = [country_scores[subj] for subj in valid_subjects]
subject_labels = [subject_names[subj] for subj in valid_subjects]
# Colores modernos para las barras
colors = ['#e74c3c', '#3498db', '#2ecc71'][:len(valid_subjects)]
bars = ax.bar(subject_labels, subject_scores, color=colors,
alpha=0.8, edgecolor='white', linewidth=2)
# Añadir valores en las barras con mejor estilo
for bar, score in zip(bars, subject_scores):
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height + 10,
f'{score}', ha='center', va='bottom',
fontweight='bold', fontsize=14, color='#2c3e50')
# Mejorar diseño
ax.set_ylabel('Puntaje PISA', fontsize=14, fontweight='bold', color='#2c3e50')
ax.set_title(f'📊 Rendimiento de {selected_country} por Materia',
fontsize=16, fontweight='bold', color='#2c3e50', pad=20)
ax.set_ylim(0, max(subject_scores) + 60)
# Grid y estilo
ax.grid(True, alpha=0.3, axis='y')
ax.set_axisbelow(True)
for spine in ax.spines.values():
spine.set_color('#bdc3c7')
plt.tight_layout()
st.pyplot(fig)
else:
st.info(f"Solo hay datos disponibles para una materia en {selected_country}")