CSVExcelReader / app.py
VicGerardoPR's picture
Update app.py
37e1481 verified
import os
import io
import unicodedata
import pandas as pd
import streamlit as st
import plotly.express as px
# -----------------------------
# Configuración de la página
# -----------------------------
st.set_page_config(
page_title="Employee Count Dashboard",
page_icon="📊",
layout="wide",
)
# -----------------------------
# Utilidades
# -----------------------------
def normalize(s: str) -> str:
s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode("utf-8", "ignore")
return " ".join(s.lower().split())
def find_target_column(df: pd.DataFrame, target="extskhis_emp full name") -> str | None:
norm_map = {col: normalize(col) for col in df.columns}
for col, norm in norm_map.items():
if norm == normalize(target):
return col
candidates = [c for c, n in norm_map.items() if "full" in n and "name" in n]
return candidates[0] if candidates else None
@st.cache_data(show_spinner=False)
def load_csv(file_obj) -> pd.DataFrame:
return pd.read_csv(file_obj)
@st.cache_data(show_spinner=False)
def load_sample(sample_path: str) -> pd.DataFrame | None:
if os.path.exists(sample_path):
return pd.read_csv(sample_path)
return None
def pretty_number(n: int) -> str:
return f"{n:,}".replace(",", " ")
# -----------------------------
# Estilos (UI)
# -----------------------------
CUSTOM_CSS = """
<style>
.stApp { background-color: #0d1117; color: #e5e7eb; }
.block-container { padding-top: 1.5rem; }
/* Tarjetas KPI */
.kpi-card {
border-radius: 14px;
padding: 18px 20px;
background: #161b22;
border: 1px solid #30363d;
box-shadow: 0 8px 16px -10px rgba(0,0,0,0.8);
}
.kpi-label { font-size: 0.85rem; color: #9ca3af; margin-bottom: 6px; }
.kpi-value { font-size: 1.6rem; font-weight: 700; color: #f9fafb; }
/* Sección principal */
.section-card {
border-radius: 16px;
padding: 20px;
background: #161b22;
border: 1px solid #30363d;
box-shadow: 0 12px 24px -16px rgba(0,0,0,0.9);
}
/* Título con acento */
h1 span.accent {
background: linear-gradient(90deg, #60a5fa, #34d399, #fbbf24);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
</style>
"""
st.markdown(CUSTOM_CSS, unsafe_allow_html=True)
# -----------------------------
# Sidebar (carga de datos)
# -----------------------------
st.sidebar.title("⚙️ Configuración")
uploaded = st.sidebar.file_uploader("Sube tu archivo CSV", type=["csv"])
sample_note = st.sidebar.empty()
df = None
source_label = ""
if uploaded is not None:
try:
data_bytes = uploaded.getvalue()
df = load_csv(io.BytesIO(data_bytes))
source_label = f"Fuente: Archivo subido — **{uploaded.name}**"
except Exception as e:
st.sidebar.error(f"Error al leer el CSV: {e}")
else:
df_sample = load_sample("data/sample.csv")
if df_sample is not None:
df = df_sample
source_label = "Fuente: `data/sample.csv` (muestra)"
sample_note.info("No subiste archivo. Mostrando ejemplo.")
else:
sample_note.warning("No subiste archivo y no existe `data/sample.csv`. Sube un CSV para continuar.")
# -----------------------------
# UI principal
# -----------------------------
st.title("📊 Employee Count Dashboard", anchor=False)
if df is None or df.empty:
st.info("Sube un CSV con la columna **EXTSKHIS_EMP FULL NAME** para ver el dashboard.")
st.stop()
target_col = find_target_column(df, "EXTSKHIS_EMP FULL NAME")
if target_col is None:
st.error("No se encontró la columna requerida.")
st.write("Columnas detectadas:", list(df.columns))
st.stop()
df[target_col] = df[target_col].astype(str).str.strip()
# -----------------------------
# Controles
# -----------------------------
with st.sidebar:
st.divider()
st.subheader("Filtros")
search = st.text_input("Filtrar por nombre", placeholder="Ej: Maria, Juan...")
min_count = st.number_input("Mínimo de ocurrencias", min_value=1, value=1, step=1)
top_n = st.slider("Mostrar Top N", min_value=5, max_value=100, value=20, step=5)
sort_mode = st.radio("Orden", ["Por conteo (desc)", "Alfabético (A→Z)"], index=0)
df_filtered = df
if search:
s = search.lower()
df_filtered = df[df[target_col].str.lower().str.contains(s, na=False)]
counts = df_filtered.groupby(target_col, dropna=False).size().reset_index(name="Count")
counts = counts[counts["Count"] >= min_count]
if sort_mode == "Por conteo (desc)":
counts = counts.sort_values("Count", ascending=False)
else:
counts = counts.sort_values(target_col, ascending=True)
counts_top = counts.head(top_n)
# -----------------------------
# KPIs
# -----------------------------
c1, c2, c3 = st.columns(3)
with c1:
st.markdown("<div class='kpi-card'>"
"<div class='kpi-label'>Registros totales</div>"
f"<div class='kpi-value'>{pretty_number(len(df))}</div>"
"</div>", unsafe_allow_html=True)
with c2:
st.markdown("<div class='kpi-card'>"
"<div class='kpi-label'>Nombres únicos</div>"
f"<div class='kpi-value'>{pretty_number(counts[target_col].nunique())}</div>"
"</div>", unsafe_allow_html=True)
with c3:
st.markdown("<div class='kpi-card'>"
"<div class='kpi-label'>Mostrando en gráfico</div>"
f"<div class='kpi-value'>{pretty_number(len(counts_top))}</div>"
"</div>", unsafe_allow_html=True)
st.caption(source_label)
# -----------------------------
# Gráfico (vertical, colorido, fondo negro)
# -----------------------------
st.markdown("### 🎨 Unidades hechas por cada operador")
if counts_top.empty:
st.warning("No hay filas que cumplan los filtros actuales.")
else:
fig = px.bar(
counts_top,
x=target_col,
y="Count",
text="Count",
color=target_col, # cada barra con color distinto
color_discrete_sequence=px.colors.qualitative.Bold, # paleta llamativa
height=600,
)
fig.update_traces(textposition="outside")
fig.update_layout(
xaxis_title="Nombre",
yaxis_title="Conteo",
plot_bgcolor="#0d1117",
paper_bgcolor="#0d1117",
font=dict(color="white"),
margin=dict(l=10, r=10, t=30, b=50),
)
st.plotly_chart(fig, use_container_width=True)
# -----------------------------
# Tabla
# -----------------------------
with st.expander("📄 Ver tabla de conteos"):
st.dataframe(counts.reset_index(drop=True), use_container_width=True)
# -----------------------------
# Descargar
# -----------------------------
csv_bytes = counts.to_csv(index=False).encode("utf-8")
st.download_button(
"⬇️ Descargar conteos (CSV)",
data=csv_bytes,
file_name="employee_counts.csv",
mime="text/csv",
)