DEP / app.py
Arturo3116's picture
Actualiza DEP.csv y lógica de app.py
8dfb18a
# app.py
import gradio as gr
import pandas as pd
import numpy as np
import plotly.graph_objects as go
# =============================================================
# CONFIGURACIÓN BÁSICA
# =============================================================
CSV_PATH = "DEP.csv"
COL_SX = "SEXO"
COL_BREED = "CRIADOR"
COL_ID = "NUMEROHATO"
COL_REG = "REGISTRO"
COL_MGT = "MGT"
# Métricas (ajusta si tu CSV difiere)
METRIC_MAP = {
"Peso al nacimiento": "DEPN",
"Peso al destete (205d)": "DEPP205",
"Leche": "DEPLECHE",
"Materno total": "DEPMATERNOTOTAL",
"Peso al año (365d)": "DEPP365",
"CE al año": "DEPCEA",
"Peso a 18 meses (550d)": "DEPP550",
"CE a 18 meses (550d)": "DEPCE550",
"Mérito genético total": "MGT",
}
EX_MAP = {
"Peso al nacimiento": "EXPN",
"Peso al destete (205d)": "EXP205",
"Leche": "EXLECHE",
"Materno total": None,
"Peso al año (365d)": "EXP365",
"CE al año": "EXCEA",
"Peso a 18 meses (550d)": "EXP550",
"CE a 18 meses (550d)": "EXCE550",
"Mérito genético total": None,
}
RADAR_ORDER = list(METRIC_MAP.keys())
BASE_COLS = [COL_ID, COL_REG, COL_SX, COL_BREED, COL_MGT]
TABLE_METRIC_COLS = [METRIC_MAP[k] for k in RADAR_ORDER if METRIC_MAP[k] != COL_MGT] + [COL_MGT]
DISPLAY_COLS = BASE_COLS + [c for c in TABLE_METRIC_COLS if c not in BASE_COLS]
# Paleta
COLOR_A = "#46973d"
COLOR_B = "#ddb516"
NEUTRAL = "#dcdcdc"
# =============================================================
# UTILIDADES DE NORMALIZACIÓN
# =============================================================
def normalize_name(s: str) -> str:
if s is None or (isinstance(s, float) and pd.isna(s)): return ""
s = str(s).strip()
while s.endswith("."):
s = s[:-1].rstrip()
s = " ".join(s.split())
return s
# =============================================================
# 🔐 CONTRASEÑAS (EDITABLE EN UN SOLO LUGAR)
# -------------------------------------------------------------
# IMPORTANTE: Las llaves deben estar ya "normalizadas" como arriba.
PASSWORDS = {
normalize_name("BEECHE_BRAHMANS_S_A"): "BEECHE2025",
normalize_name("GANADERIA_COLONO_REAL_S_A"): "COLONOREAL2025",
normalize_name("ALVARO_CLACHAR_B_Y_O_HDA_EL_REAL_S_A"): "CLACHAR2025",
normalize_name("HDA_EL_ESCUDO_ANTONIO_RODRIGUEZ_S_A"): "ESCUDO2025",
normalize_name("ANGUIZOLA_M_SERGIO"): "ANGUIZOLA2025",
normalize_name("CARLOS_LEE"): "LEE2025",
normalize_name("GANADERA_GUACHIPELIN_DEL_SUR_S_A"): "GUACHIPELIN2025",
normalize_name("GANADERA_KARLA_MARY_S_A"): "KARLAMARY2025",
normalize_name("GANADERA_SAVAL_S_A"): "SAVAL2025",
normalize_name("GANADERIA_JL_MORALES_FINCA_EL_ROSARIO_S"): "MORALES2025",
normalize_name("GRUPO_MAG_NICARAGUA"): "MAGNICARAGUA2025",
normalize_name("HACIENDA_PANDORA_S_A"): "PANDORA2025",
normalize_name("HERITAGE_CATTLE_COMPANY_HUNGERFORD"): "HERITAGE2025",
normalize_name("HERMACOR_S_A"): "HERMACOR2025",
normalize_name("HK_CATTLE"): "HKCATTLE2025",
normalize_name("JESSICA_SMITH"): "SMITH2025",
normalize_name("JIM_S_WILLIAMS"): "WILLIAMS2025",
normalize_name("LA_PRADERA_DEL_NORTE_LTDA"): "PRADERA2025",
normalize_name("LA_PRECIOSA_S_A"): "PRECIOSA2025",
normalize_name("LESLIE_W_HUDGINS"): "HUDGINS2025",
normalize_name("MEGAN_ELISE_CULLERS"): "CULLERS2025",
normalize_name("PRODUCTOS_PEDREGAL_S_A"): "PEDREGAL2025",
normalize_name("RAIMUNDO_RIOJAS_Y_O_REGINA_DE_R"): "RIOJASREGINA2025",
normalize_name("RANCHO_SIGUACAN_EN_SEVILLA"): "SIGUACAN2025",
normalize_name("SEMINOLE_S_A"): "SEMINOLE2025",
normalize_name("V8_RANCH"): "V8RANCH2025",
normalize_name("ARIAS_Z_JOSE_RAMON"): "ARIAS2025",
normalize_name("ECOS_DEL_PORVENIR_S_A"): "ECOSDELPORVENIR2025",
normalize_name("GANADERIA_SAN_CRIST_BAL_S_A"): "SANCRISTOBAL2025",
normalize_name("INTA_EJN"): "INTA",
normalize_name("GANADERA_HURTADO_LTDA"): "HURTADO2025",
normalize_name("FRANZ_W_HEINSOHN_MONTERO"): "HEINSOHNMONTERO2025",
normalize_name("HACIENDA_SOLIMAR_LTDA"): "SOLIMAR2025",
normalize_name("ESC_DE_AGRICULTURA_REGION_TROP_HUMEDA"): "AGRICULTURATROPICAL2025",
normalize_name("AGRICOLA_EL_CANTARO_S_A"): "CANTARO2025",
normalize_name("MURILLO_KOPPER_TULIO"): "MURILLOKOPPER2025",
normalize_name("AGROPECUARIA_EL_DORADO_S_A"): "ELDORADO2025",
normalize_name("CIA_GANADERA_GUACIMAL_S_A"): "GUACIMAL2025",
normalize_name("JOSE_E_BONILLA_ESQUIVEL"): "JOSEBONILLAESQUIVEL2025",
normalize_name("SHARON_S_DE_WOLF"): "WOLF2025",
normalize_name("PATRICIO_PITTI"): "PITTI2025",
normalize_name("TULLOCH_ESTATE_LTD"): "TULLOCHESTATE2025",
normalize_name("GANADERIA_CRUZ_S_A"): "CRUZ2025",
normalize_name("HACIENDA_CHOROTEGA_S_A"): "CHOROTEGA2025",
normalize_name("GANADERIA_HERACOS_GHA_S_A"): "HERACOS2025",
normalize_name("GANADERIA_EL_ROSARIO_Y_O_MARLA_ARGUELLO"): "ELROSARIOARGUELLO2025",
normalize_name("GP_BRAHMANS"): "GPBRAHMANS2025",
normalize_name("INSTITUTO_TECNOLOGICO_DE_COSTA_RICA"): "ITCR2025",
normalize_name("HACIENDA_EL_RETIRO_LTDA"): "ELRETIRO2025",
normalize_name("INTA_LD"): "INTA",
normalize_name("DESCONOCIDO"): "DESCONOCIDO2025",
normalize_name("ESCUELA_CENTROAMERICANA_DE_GANADERIA"): "ESCUELAGANADERIA2025",
normalize_name("J_D_HUDGINS_FORGASON_DIV"): "JDHUDGINS2025",
normalize_name("SIQUIARES_S_A"): "SIQUIARES2025",
normalize_name("UNITED_STATES_SUGAR_CORP"): "USSUGAR2025",
normalize_name("SLOAN_WILLIAMS"): "SLOANWILLIAMS2025",
normalize_name("MONTANA_S_A"): "CANAGUA",
normalize_name("ANIMAR_LTDA"): "ANIMAR2025",
normalize_name("JOSE_CAMPOS_QUESADA"): "JOSECAMPOSQUESADA2025",
normalize_name("HACIENDA_SANTA_PAULA_S_A"): "SANTAPAULA2025",
normalize_name("GANADERA_ABR_S_A"): "ABR2025",
normalize_name("GANADERA_MONTERREY_S_A"): "MONTERREY2025",
normalize_name("GANADERA_XIRINACHS_BATALLA_DE_C_R_S_A"): "XIRINACHS2025",
}
def password_for_criador(criador_norm: str) -> str:
return PASSWORDS.get(criador_norm or "", "")
# =============================================================
# UTILIDADES
# =============================================================
def _coerce_numeric(df, cols):
for c in cols:
if c in df.columns:
df[c] = pd.to_numeric(df[c], errors="coerce")
return df
def _normalize_minmax(x, gmin, gmax):
s = x if isinstance(x, pd.Series) else pd.Series([x])
min_s = pd.Series([gmin] * len(s), index=s.index) if np.isscalar(gmin) else gmin.reindex(s.index, fill_value=gmin.iloc[0] if len(gmin) else 0)
max_s = pd.Series([gmax] * len(s), index=s.index) if np.isscalar(gmax) else gmax.reindex(s.index, fill_value=gmax.iloc[0] if len(gmax) else 1)
rng = (max_s - min_s).replace(0, 1)
return ((s - min_s) / rng).fillna(0.0).clip(0, 1)
def _hex_to_rgb(h): h = h.lstrip("#"); return tuple(int(h[i:i+2], 16) for i in (0,2,4))
def _rgb_to_hex(rgb): return "#%02x%02x%02x" % rgb
def _lerp_color(a_hex, b_hex, t):
a = _hex_to_rgb(a_hex); b = _hex_to_rgb(b_hex)
c = tuple(int(round(a[i] + (b[i]-a[i]) * float(t))) for i in range(3))
return _rgb_to_hex(c)
def _coerce_ex_to_unit(ex):
if ex is None: return None
try: val = float(ex)
except: return None
t = val if val <= 1.5 else val/100.0
if np.isnan(t): return None
return max(0.0, min(1.0, t))
def _build_radar_with_accuracy(values_dict, acc_dict, title):
labels = list(values_dict.keys())
values = [values_dict[k] for k in labels]
theta = labels + [labels[0]]
r_vals = values + [values[0]]
bar_colors, hovertexts = [], []
for lab in labels:
t = _coerce_ex_to_unit(acc_dict.get(lab, None))
if t is None:
bar_colors.append(NEUTRAL)
hovertexts.append(f"{lab}<br>Exactitud: —")
else:
# degradado por exactitud
bar_colors.append(_lerp_color(COLOR_A, COLOR_B, t))
hovertexts.append(f"{lab}<br>Exactitud: {int(round(t*100))}%")
fig = go.Figure()
fig.add_trace(go.Barpolar(
r=[1.0]*len(labels),
theta=labels,
marker=dict(color=bar_colors, line=dict(color="white", width=1)),
opacity=0.6,
name="Exactitud (EX)",
hovertext=hovertexts,
hovertemplate="%{hovertext}<extra></extra>",
))
fig.add_trace(go.Scatterpolar(
r=r_vals, theta=theta, mode="lines+markers", fill="toself",
name="Valor normalizado",
line=dict(color=COLOR_A, width=3),
marker=dict(size=6, color=COLOR_B),
hovertemplate="<b>%{theta}</b><br>Valor: %{r:.2f}<extra></extra>",
))
fig.update_layout(
title=title, showlegend=True, paper_bgcolor="white",
polar=dict(
bgcolor="white",
radialaxis=dict(visible=True, range=[0,1], gridcolor="#eeeeee", linecolor=COLOR_A),
angularaxis=dict(gridcolor="#f2f2f2", linecolor=COLOR_A),
),
margin=dict(l=10, r=10, t=60, b=10),
legend=dict(orientation="h", yanchor="bottom", y=1.05, xanchor="center", x=0.5),
)
return fig
# =============================================================
# CARGA DE DATOS
# =============================================================
def load_data():
df = pd.read_csv(CSV_PATH)
if COL_SX in df.columns:
df[COL_SX] = df[COL_SX].astype(str).str.upper().str.strip()
df[COL_SX] = df[COL_SX].replace({"HEMBRA":"H","MACHO":"M"})
if COL_BREED in df.columns:
df[COL_BREED] = df[COL_BREED].astype(str)
df["CRIADOR_NORM"] = df[COL_BREED].map(normalize_name)
df = _coerce_numeric(df, list(set(TABLE_METRIC_COLS + [COL_MGT])))
ex_cols = [c for c in EX_MAP.values() if c]
df = _coerce_numeric(df, ex_cols)
breeders_all = sorted([b for b in df["CRIADOR_NORM"].dropna().unique().tolist() if b not in ["", "nan"]])
metric_cols_present = [METRIC_MAP[k] for k in RADAR_ORDER if METRIC_MAP[k] in df.columns]
global_mins = df[metric_cols_present].min(numeric_only=True)
global_maxs = df[metric_cols_present].max(numeric_only=True)
return df, breeders_all, global_mins, global_maxs
# =============================================================
# CÁLCULOS
# =============================================================
def _promedios_y_exactitud(df_subset, global_mins, global_maxs):
valores, ex_vals = {}, {}
for label in RADAR_ORDER:
dep_col = METRIC_MAP[label]
ex_col = EX_MAP.get(label, None)
if dep_col in df_subset.columns:
prom_val = pd.to_numeric(df_subset[dep_col], errors="coerce").mean()
# normalizar con min-max global
nrm = (prom_val - global_mins.get(dep_col, 0)) / max((global_maxs.get(dep_col, 1) - global_mins.get(dep_col, 0)), 1e-9)
valores[label] = float(np.clip(nrm, 0, 1)) if pd.notnull(prom_val) else 0.0
else:
valores[label] = 0.0
if ex_col and ex_col in df_subset.columns:
ex_prom = pd.to_numeric(df_subset[ex_col], errors="coerce").mean()
ex_vals[label] = float(ex_prom) if pd.notnull(ex_prom) else None
else:
ex_vals[label] = None
return valores, ex_vals
def _valores_y_exactitud_fila(row, df_cols, global_mins, global_maxs):
valores, ex_vals = {}, {}
for label in RADAR_ORDER:
dep_col = METRIC_MAP[label]
ex_col = EX_MAP.get(label, None)
if dep_col in df_cols:
val = pd.to_numeric(row.get(dep_col, np.nan), errors="coerce")
if pd.notnull(val):
nrm = (val - global_mins.get(dep_col, 0)) / max((global_maxs.get(dep_col, 1) - global_mins.get(dep_col, 0)), 1e-9)
valores[label] = float(np.clip(nrm, 0, 1))
else:
valores[label] = 0.0
else:
valores[label] = 0.0
if ex_col and ex_col in df_cols:
exv = pd.to_numeric(row.get(ex_col, np.nan), errors="coerce")
ex_vals[label] = float(exv) if pd.notnull(exv) else None
else:
ex_vals[label] = None
return valores, ex_vals
def top_hembras_por_criador(df, criador_norm, global_mins, global_maxs):
dff = df.copy()
if criador_norm and criador_norm != "—":
dff = dff[dff["CRIADOR_NORM"] == criador_norm]
dff = dff[dff[COL_SX] == "H"]
dff = dff.sort_values(by=COL_MGT, ascending=False, na_position="last").head(30)
cols_exist = [c for c in DISPLAY_COLS if c in dff.columns]
tabla = dff[cols_exist].reset_index(drop=True)
valores, ex_vals = _promedios_y_exactitud(dff, global_mins, global_maxs)
titulo = f"Promedio Hembras • {criador_norm}" if criador_norm else "Promedio Hembras"
fig = _build_radar_with_accuracy(valores, ex_vals, titulo)
return tabla, fig
def top_machos_por_metrica(df, metric_label, criador_norm):
met_col = METRIC_MAP.get(metric_label, COL_MGT)
dff = df.copy()
if criador_norm and criador_norm != "—":
dff = dff[dff["CRIADOR_NORM"] == criador_norm]
dff = dff[dff[COL_SX] == "M"]
dff = dff.sort_values(by=met_col, ascending=False, na_position="last").head(50)
cols_exist = [c for c in DISPLAY_COLS if c in dff.columns]
tabla = dff[cols_exist].reset_index(drop=True)
return tabla
def radar_individual_from_selection(evt: gr.SelectData, df_machos_current, global_mins, global_maxs):
if df_machos_current is None or len(df_machos_current) == 0:
return gr.update(value=None)
try:
row_idx = evt.index[0] if isinstance(evt.index, (list, tuple)) else evt.index
except Exception:
return gr.update(value=None)
if row_idx is None or row_idx >= len(df_machos_current):
return gr.update(value=None)
row = df_machos_current.iloc[int(row_idx)]
valores, ex_vals = _valores_y_exactitud_fila(row, df_machos_current.columns, global_mins, global_maxs)
ident = str(row.get(COL_ID, "")) or "(sin ID)"
fig = _build_radar_with_accuracy(valores, ex_vals, f"Radar individuo • {ident}")
return fig
# =============================================================
# INTERFAZ
# =============================================================
df_global, breeders_all, global_mins, global_maxs = load_data()
# Núcleo visible por defecto (opcional)
core_breeders = []
for name in ["INTA-EJN", "INTA-LOS DIAMANTES"]:
core_breeders.append(normalize_name(name))
core_breeders = sorted(list(dict.fromkeys([b for b in core_breeders if b])))
init_criador = core_breeders[0] if core_breeders else "—"
init_tabla_h, init_fig_h = top_hembras_por_criador(df_global, init_criador, global_mins, global_maxs)
init_tabla_m = top_machos_por_metrica(df_global, "Mérito genético total", init_criador)
def _auth_message(ok: bool, criador_norm: str) -> str:
if not criador_norm or criador_norm == "—":
return "Seleccione un CRIADOR."
if ok:
return f"✅ Acceso concedido para **{criador_norm}**."
else:
return f"🔒 Contraseña incorrecta para **{criador_norm}**."
def _is_authorized(criador_norm: str, pwd_input: str) -> bool:
if not criador_norm or criador_norm == "—":
return False
return (pwd_input or "") == password_for_criador(criador_norm)
with gr.Blocks(title="Programa Nacional de Evaluación y Mejoramiento Genético") as demo:
# CSS
gr.HTML(f"""
<style>
:root {{ color-scheme: light; }}
body, .gradio-container, .prose, .prose * {{
color: #DDB516 !important;
background: #ffffff !important;
}}
h1, h2, h3, .prose h1, .prose h2, .prose h3 {{
color: #DDB516 !important;
}}
.gr-button, button {{ border-radius: 10px; }}
.gr-input, .gr-textbox, .gr-dropdown {{ border-color: {COLOR_A}; }}
.gradio-container a {{ color: {COLOR_B}; }}
.notice {{ font-size:14px; margin-top:6px; opacity:.9; }}
</style>
""")
# Encabezado solicitado
gr.Markdown("# Programa Nacional de Evaluación y Mejoramiento Genético")
gr.Markdown(
"En el año 2002 da inicio el Proyecto de Evaluación Genética de Bovinos de Carne registrados en Costa Rica, "
"con la captura de datos de pesos de hembras y machos al destete, al año y a 18 meses, así como la medición de "
"la circunferencia escrotal en machos, en fincas de criadores de Bovinos de las razas cebuinas, principalmente "
"Brahman y Nelore. Este proyecto nació como iniciativa de la Corporación Ganadera- CORFOGA en cooperación con la "
"Escuela de Ciencias Agrarias de la Universidad Nacional de Costa Rica-UNA y con la colaboración del Instituto de "
"Innovación y Transferencia en Tecnología Agropecuaria-INTA y la Asociación de Criadores de Ganado Cebú de Costa Rica –ASOCEBU. Si quiere probar puede visualizar Fincas del Inta con el codigo INTA"
)
gr.Markdown("## Selección y acceso por **CRIADOR**")
with gr.Row():
dd_criador = gr.Dropdown(
choices=["—"] + breeders_all if breeders_all else ["—"],
value=(["—"] + core_breeders)[1] if core_breeders else "—",
label="CRIADOR (deduplicado)"
)
tb_password = gr.Textbox(
label="Contraseña de la finca seleccionada",
placeholder="Ingrese la contraseña del CRIADOR",
type="password",
value=""
)
auth_msg = gr.Markdown("", elem_classes=["notice"])
gr.Markdown("Filtra **hembras** y **machos** por **CRIADOR** (la finca seleccionada). "
"Para ver los datos, ingrese la contraseña de la finca.")
machos_state = gr.State(value=init_tabla_m)
# ---- HEMBRAS ----
with gr.Group():
gr.Markdown("## Hembras por **CRIADOR** (Top 30 por MGT)")
with gr.Row():
out_tabla_hembras = gr.Dataframe(value=init_tabla_h, label="Hembras (Top 30 por MGT)", interactive=False)
out_radar_hembras = gr.Plot(value=init_fig_h, label="Telaraña: Promedios Hembras (anillo = EX)")
# ---- MACHOS ----
with gr.Group():
gr.Markdown("## Machos (Top 50) por métrica — filtrados por el mismo **CRIADOR**")
metrica_rank = gr.Dropdown(
choices=list(METRIC_MAP.keys()),
value="Mérito genético total",
label="Métrica para rankear machos"
)
out_tabla_machos = gr.Dataframe(value=init_tabla_m, label="Machos (Top 50)", interactive=False)
out_radar_macho = gr.Plot(label="Telaraña: Individuo (anillo = EX)")
# =========================================================
# CALLBACKS
# =========================================================
def update_all(criador_norm, pwd, metric_label):
ok = _is_authorized(criador_norm, pwd)
msg = _auth_message(ok, criador_norm)
if not ok:
empty_df = pd.DataFrame(columns=[c for c in DISPLAY_COLS if c in df_global.columns])
return (
msg,
empty_df, gr.update(value=None),
empty_df, empty_df, gr.update(value=None)
)
# Autorizado: actualizar hembras y machos filtrados por la misma finca
tabla_h, fig_h = top_hembras_por_criador(df_global, criador_norm if criador_norm != "—" else None, global_mins, global_maxs)
tabla_m = top_machos_por_metrica(df_global, metric_label, criador_norm if criador_norm != "—" else None)
return (
msg,
tabla_h, fig_h,
tabla_m, tabla_m, gr.update()
)
dd_criador.change(
fn=update_all,
inputs=[dd_criador, tb_password, metrica_rank],
outputs=[auth_msg, out_tabla_hembras, out_radar_hembras, out_tabla_machos, machos_state, out_radar_macho]
)
tb_password.change(
fn=update_all,
inputs=[dd_criador, tb_password, metrica_rank],
outputs=[auth_msg, out_tabla_hembras, out_radar_hembras, out_tabla_machos, machos_state, out_radar_macho]
)
metrica_rank.change(
fn=update_all,
inputs=[dd_criador, tb_password, metrica_rank],
outputs=[auth_msg, out_tabla_hembras, out_radar_hembras, out_tabla_machos, machos_state, out_radar_macho]
)
def on_select_macho(evt: gr.SelectData, df_machos_current):
return radar_individual_from_selection(evt, df_machos_current, global_mins, global_maxs)
out_tabla_machos.select(
fn=on_select_macho,
inputs=[machos_state],
outputs=[out_radar_macho]
)
gr.Markdown(
f"<div style='text-align:center; font-size: 14px; margin-top: 12px; opacity:0.85;'>"
f"Elaborado por <b>Departamento de Proyectos Corfoga</b> • Paleta "
f"<span style='color:{COLOR_A}'>{COLOR_A}</span> / <span style='color:{COLOR_B}'>{COLOR_B}</span>"
f"</div>"
)
if __name__ == "__main__":
demo.launch()