VicGerardoPR commited on
Commit
d4df948
·
verified ·
1 Parent(s): 7550cc8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +33 -56
app.py CHANGED
@@ -18,17 +18,14 @@ st.set_page_config(
18
  # Utilidades
19
  # -----------------------------
20
  def normalize(s: str) -> str:
21
- """Normaliza un nombre de columna: minúsculas, sin acentos, sin dobles espacios."""
22
  s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode("utf-8", "ignore")
23
  return " ".join(s.lower().split())
24
 
25
  def find_target_column(df: pd.DataFrame, target="extskhis_emp full name") -> str | None:
26
- """Encuentra la columna objetivo, siendo tolerante a acentos/espacios/caso."""
27
  norm_map = {col: normalize(col) for col in df.columns}
28
  for col, norm in norm_map.items():
29
  if norm == normalize(target):
30
  return col
31
- # fallback: columnas muy parecidas
32
  candidates = [c for c, n in norm_map.items() if "full" in n and "name" in n]
33
  return candidates[0] if candidates else None
34
 
@@ -46,38 +43,36 @@ def pretty_number(n: int) -> str:
46
  return f"{n:,}".replace(",", " ")
47
 
48
  # -----------------------------
49
- # Estilos (UI) – look limpio
50
  # -----------------------------
51
  CUSTOM_CSS = """
52
  <style>
53
- /* Fondo suave y tarjetas con glass effect */
54
- .stApp { background: linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%); }
55
  .block-container { padding-top: 1.5rem; }
56
 
57
  /* Tarjetas KPI */
58
  .kpi-card {
59
  border-radius: 14px;
60
  padding: 18px 20px;
61
- background: rgba(255, 255, 255, 0.75);
62
- backdrop-filter: blur(6px);
63
- border: 1px solid rgba(0,0,0,0.06);
64
- box-shadow: 0 10px 20px -12px rgba(0,0,0,0.12);
65
  }
66
- .kpi-label { font-size: 0.85rem; color: #5b6573; margin-bottom: 6px; }
67
- .kpi-value { font-size: 1.6rem; font-weight: 700; color: #111827; }
68
 
69
- /* Contenedor principal */
70
  .section-card {
71
  border-radius: 16px;
72
  padding: 20px;
73
- background: #ffffff;
74
- border: 1px solid #e5e7eb;
75
- box-shadow: 0 12px 24px -16px rgba(0,0,0,0.18);
76
  }
77
 
78
  /* Título con acento */
79
  h1 span.accent {
80
- background: linear-gradient(90deg, #2563eb, #06b6d4);
81
  -webkit-background-clip: text;
82
  -webkit-text-fill-color: transparent;
83
  }
@@ -93,24 +88,21 @@ st.sidebar.title("⚙️ Configuración")
93
  uploaded = st.sidebar.file_uploader("Sube tu archivo CSV", type=["csv"])
94
  sample_note = st.sidebar.empty()
95
 
96
- # Carga de datos: CSV subido o fallback sample
97
  df = None
98
  source_label = ""
99
  if uploaded is not None:
100
  try:
101
- # Usa bytes para mantener cache estable
102
  data_bytes = uploaded.getvalue()
103
  df = load_csv(io.BytesIO(data_bytes))
104
  source_label = f"Fuente: Archivo subido — **{uploaded.name}**"
105
  except Exception as e:
106
  st.sidebar.error(f"Error al leer el CSV: {e}")
107
  else:
108
- # intenta cargar sample
109
  df_sample = load_sample("data/sample.csv")
110
  if df_sample is not None:
111
  df = df_sample
112
  source_label = "Fuente: `data/sample.csv` (muestra)"
113
- sample_note.info("No subiste archivo. Mostrando un ejemplo desde `data/sample.csv`.")
114
  else:
115
  sample_note.warning("No subiste archivo y no existe `data/sample.csv`. Sube un CSV para continuar.")
116
 
@@ -125,11 +117,10 @@ if df is None or df.empty:
125
 
126
  target_col = find_target_column(df, "EXTSKHIS_EMP FULL NAME")
127
  if target_col is None:
128
- st.error("No se encontró la columna **EXTSKHIS_EMP FULL NAME** (o equivalente). Verifica los encabezados.")
129
  st.write("Columnas detectadas:", list(df.columns))
130
  st.stop()
131
 
132
- # Limpieza básica del campo (opcional)
133
  df[target_col] = df[target_col].astype(str).str.strip()
134
 
135
  # -----------------------------
@@ -138,34 +129,24 @@ df[target_col] = df[target_col].astype(str).str.strip()
138
  with st.sidebar:
139
  st.divider()
140
  st.subheader("Filtros")
141
- search = st.text_input("Filtrar por nombre (contiene)", placeholder="Ej: Maria, Juan...")
142
  min_count = st.number_input("Mínimo de ocurrencias", min_value=1, value=1, step=1)
143
  top_n = st.slider("Mostrar Top N", min_value=5, max_value=100, value=20, step=5)
144
  sort_mode = st.radio("Orden", ["Por conteo (desc)", "Alfabético (A→Z)"], index=0)
145
 
146
- # Aplica filtro de texto
147
  df_filtered = df
148
  if search:
149
  s = search.lower()
150
  df_filtered = df[df[target_col].str.lower().str.contains(s, na=False)]
151
 
152
- # Agrega conteos
153
- counts = (
154
- df_filtered.groupby(target_col, dropna=False)
155
- .size()
156
- .reset_index(name="Count")
157
- )
158
-
159
- # Filtra por mínimo de ocurrencias
160
  counts = counts[counts["Count"] >= min_count]
161
 
162
- # Ordena
163
  if sort_mode == "Por conteo (desc)":
164
  counts = counts.sort_values("Count", ascending=False)
165
  else:
166
  counts = counts.sort_values(target_col, ascending=True)
167
 
168
- # Top N
169
  counts_top = counts.head(top_n)
170
 
171
  # -----------------------------
@@ -188,48 +169,44 @@ with c3:
188
  f"<div class='kpi-value'>{pretty_number(len(counts_top))}</div>"
189
  "</div>", unsafe_allow_html=True)
190
 
191
- # Fuente de datos
192
  st.caption(source_label)
193
 
194
  # -----------------------------
195
- # Gráfico
196
  # -----------------------------
197
- st.markdown("### 🔎 Conteo por **EXTSKHIS_EMP FULL NAME**")
198
 
199
  if counts_top.empty:
200
  st.warning("No hay filas que cumplan los filtros actuales.")
201
  else:
202
- # Para mejorar legibilidad en barras, hacemos categoría ordenada
203
- category_order = counts_top.sort_values(
204
- "Count", ascending=False
205
- )[target_col].tolist()
206
-
207
  fig = px.bar(
208
  counts_top,
209
- x="Count",
210
- y=target_col,
211
- orientation="h",
212
  text="Count",
213
- category_orders={target_col: category_order},
214
- height=600 if len(counts_top) <= 25 else 800,
 
215
  )
216
- fig.update_traces(textposition="outside", cliponaxis=False)
217
  fig.update_layout(
218
- xaxis_title="Conteo",
219
- yaxis_title="Nombre",
220
- margin=dict(l=10, r=10, t=30, b=10),
221
- bargap=0.25,
 
 
222
  )
223
- st.plotly_chart(fig, use_container_width=True, theme="streamlit")
224
 
225
  # -----------------------------
226
- # Tabla de detalle
227
  # -----------------------------
228
  with st.expander("📄 Ver tabla de conteos"):
229
  st.dataframe(counts.reset_index(drop=True), use_container_width=True)
230
 
231
  # -----------------------------
232
- # Descargar resultados
233
  # -----------------------------
234
  csv_bytes = counts.to_csv(index=False).encode("utf-8")
235
  st.download_button(
 
18
  # Utilidades
19
  # -----------------------------
20
  def normalize(s: str) -> str:
 
21
  s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode("utf-8", "ignore")
22
  return " ".join(s.lower().split())
23
 
24
  def find_target_column(df: pd.DataFrame, target="extskhis_emp full name") -> str | None:
 
25
  norm_map = {col: normalize(col) for col in df.columns}
26
  for col, norm in norm_map.items():
27
  if norm == normalize(target):
28
  return col
 
29
  candidates = [c for c, n in norm_map.items() if "full" in n and "name" in n]
30
  return candidates[0] if candidates else None
31
 
 
43
  return f"{n:,}".replace(",", " ")
44
 
45
  # -----------------------------
46
+ # Estilos (UI)
47
  # -----------------------------
48
  CUSTOM_CSS = """
49
  <style>
50
+ .stApp { background-color: #0d1117; color: #e5e7eb; }
 
51
  .block-container { padding-top: 1.5rem; }
52
 
53
  /* Tarjetas KPI */
54
  .kpi-card {
55
  border-radius: 14px;
56
  padding: 18px 20px;
57
+ background: #161b22;
58
+ border: 1px solid #30363d;
59
+ box-shadow: 0 8px 16px -10px rgba(0,0,0,0.8);
 
60
  }
61
+ .kpi-label { font-size: 0.85rem; color: #9ca3af; margin-bottom: 6px; }
62
+ .kpi-value { font-size: 1.6rem; font-weight: 700; color: #f9fafb; }
63
 
64
+ /* Sección principal */
65
  .section-card {
66
  border-radius: 16px;
67
  padding: 20px;
68
+ background: #161b22;
69
+ border: 1px solid #30363d;
70
+ box-shadow: 0 12px 24px -16px rgba(0,0,0,0.9);
71
  }
72
 
73
  /* Título con acento */
74
  h1 span.accent {
75
+ background: linear-gradient(90deg, #60a5fa, #34d399, #fbbf24);
76
  -webkit-background-clip: text;
77
  -webkit-text-fill-color: transparent;
78
  }
 
88
  uploaded = st.sidebar.file_uploader("Sube tu archivo CSV", type=["csv"])
89
  sample_note = st.sidebar.empty()
90
 
 
91
  df = None
92
  source_label = ""
93
  if uploaded is not None:
94
  try:
 
95
  data_bytes = uploaded.getvalue()
96
  df = load_csv(io.BytesIO(data_bytes))
97
  source_label = f"Fuente: Archivo subido — **{uploaded.name}**"
98
  except Exception as e:
99
  st.sidebar.error(f"Error al leer el CSV: {e}")
100
  else:
 
101
  df_sample = load_sample("data/sample.csv")
102
  if df_sample is not None:
103
  df = df_sample
104
  source_label = "Fuente: `data/sample.csv` (muestra)"
105
+ sample_note.info("No subiste archivo. Mostrando ejemplo.")
106
  else:
107
  sample_note.warning("No subiste archivo y no existe `data/sample.csv`. Sube un CSV para continuar.")
108
 
 
117
 
118
  target_col = find_target_column(df, "EXTSKHIS_EMP FULL NAME")
119
  if target_col is None:
120
+ st.error("No se encontró la columna requerida.")
121
  st.write("Columnas detectadas:", list(df.columns))
122
  st.stop()
123
 
 
124
  df[target_col] = df[target_col].astype(str).str.strip()
125
 
126
  # -----------------------------
 
129
  with st.sidebar:
130
  st.divider()
131
  st.subheader("Filtros")
132
+ search = st.text_input("Filtrar por nombre", placeholder="Ej: Maria, Juan...")
133
  min_count = st.number_input("Mínimo de ocurrencias", min_value=1, value=1, step=1)
134
  top_n = st.slider("Mostrar Top N", min_value=5, max_value=100, value=20, step=5)
135
  sort_mode = st.radio("Orden", ["Por conteo (desc)", "Alfabético (A→Z)"], index=0)
136
 
 
137
  df_filtered = df
138
  if search:
139
  s = search.lower()
140
  df_filtered = df[df[target_col].str.lower().str.contains(s, na=False)]
141
 
142
+ counts = df_filtered.groupby(target_col, dropna=False).size().reset_index(name="Count")
 
 
 
 
 
 
 
143
  counts = counts[counts["Count"] >= min_count]
144
 
 
145
  if sort_mode == "Por conteo (desc)":
146
  counts = counts.sort_values("Count", ascending=False)
147
  else:
148
  counts = counts.sort_values(target_col, ascending=True)
149
 
 
150
  counts_top = counts.head(top_n)
151
 
152
  # -----------------------------
 
169
  f"<div class='kpi-value'>{pretty_number(len(counts_top))}</div>"
170
  "</div>", unsafe_allow_html=True)
171
 
 
172
  st.caption(source_label)
173
 
174
  # -----------------------------
175
+ # Gráfico (vertical, colorido, fondo negro)
176
  # -----------------------------
177
+ st.markdown("### 🎨 Conteo por **EXTSKHIS_EMP FULL NAME**")
178
 
179
  if counts_top.empty:
180
  st.warning("No hay filas que cumplan los filtros actuales.")
181
  else:
 
 
 
 
 
182
  fig = px.bar(
183
  counts_top,
184
+ x=target_col,
185
+ y="Count",
 
186
  text="Count",
187
+ color=target_col, # cada barra con color distinto
188
+ color_discrete_sequence=px.colors.qualitative.Bold, # paleta llamativa
189
+ height=600,
190
  )
191
+ fig.update_traces(textposition="outside")
192
  fig.update_layout(
193
+ xaxis_title="Nombre",
194
+ yaxis_title="Conteo",
195
+ plot_bgcolor="#0d1117",
196
+ paper_bgcolor="#0d1117",
197
+ font=dict(color="white"),
198
+ margin=dict(l=10, r=10, t=30, b=50),
199
  )
200
+ st.plotly_chart(fig, use_container_width=True)
201
 
202
  # -----------------------------
203
+ # Tabla
204
  # -----------------------------
205
  with st.expander("📄 Ver tabla de conteos"):
206
  st.dataframe(counts.reset_index(drop=True), use_container_width=True)
207
 
208
  # -----------------------------
209
+ # Descargar
210
  # -----------------------------
211
  csv_bytes = counts.to_csv(index=False).encode("utf-8")
212
  st.download_button(