hypertunercer 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hypertunercer-0.1.0/PKG-INFO +142 -0
- hypertunercer-0.1.0/README.md +103 -0
- hypertunercer-0.1.0/hypertuner/__init__.py +9 -0
- hypertunercer-0.1.0/hypertuner/tuner.py +644 -0
- hypertunercer-0.1.0/hypertunercer.egg-info/PKG-INFO +142 -0
- hypertunercer-0.1.0/hypertunercer.egg-info/SOURCES.txt +9 -0
- hypertunercer-0.1.0/hypertunercer.egg-info/dependency_links.txt +1 -0
- hypertunercer-0.1.0/hypertunercer.egg-info/requires.txt +18 -0
- hypertunercer-0.1.0/hypertunercer.egg-info/top_level.txt +1 -0
- hypertunercer-0.1.0/pyproject.toml +61 -0
- hypertunercer-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hypertunercer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Playground interactivo de hiperparámetros para modelos sklearn en Streamlit
|
|
5
|
+
Author: Jesús Alberto Cerón Hernández
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/tu-ceronbuu/hypertuner
|
|
8
|
+
Project-URL: Repository, https://github.com/tu-usuario/hypertuner
|
|
9
|
+
Project-URL: Issues, https://github.com/tu-usuario/hypertuner/issues
|
|
10
|
+
Keywords: machine-learning,hyperparameter-tuning,streamlit,scikit-learn,visualization,interactive
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: streamlit>=1.30
|
|
25
|
+
Requires-Dist: scikit-learn>=1.3
|
|
26
|
+
Requires-Dist: matplotlib>=3.7
|
|
27
|
+
Requires-Dist: pandas>=2.0
|
|
28
|
+
Requires-Dist: numpy>=1.24
|
|
29
|
+
Provides-Extra: xgboost
|
|
30
|
+
Requires-Dist: xgboost>=2.0; extra == "xgboost"
|
|
31
|
+
Provides-Extra: lightgbm
|
|
32
|
+
Requires-Dist: lightgbm>=4.0; extra == "lightgbm"
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: pytest; extra == "dev"
|
|
35
|
+
Requires-Dist: black; extra == "dev"
|
|
36
|
+
Requires-Dist: ruff; extra == "dev"
|
|
37
|
+
Requires-Dist: build; extra == "dev"
|
|
38
|
+
Requires-Dist: twine; extra == "dev"
|
|
39
|
+
|
|
40
|
+
# ⚗ hypertunercer
|
|
41
|
+
|
|
42
|
+
**Playground interactivo de hiperparámetros para cualquier clasificador scikit-learn.**
|
|
43
|
+
|
|
44
|
+
HyperTuner lanza una interfaz Streamlit completa con fronteras de decisión, curvas ROC y métricas en tiempo real — todo generado automáticamente desde un diccionario de configuración.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Instalación
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install hypertunercer
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Con soporte para XGBoost o LightGBM:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install "hypertunercer[xgboost]"
|
|
58
|
+
pip install "hypertunercer[lightgbm]"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Uso rápido
|
|
64
|
+
|
|
65
|
+
Crea un archivo `app.py` con esto:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from hypertunercer import tuner_universal
|
|
69
|
+
from sklearn.ensemble import RandomForestClassifier
|
|
70
|
+
from sklearn.datasets import make_classification
|
|
71
|
+
|
|
72
|
+
X, y = make_classification(n_samples=500, n_features=10, random_state=42)
|
|
73
|
+
|
|
74
|
+
param_grid = {
|
|
75
|
+
"n_estimators": {"type": "slider", "min": 10, "max": 300, "default": 100, "step": 10},
|
|
76
|
+
"max_depth": {"type": "slider", "min": 1, "max": 20, "default": 5},
|
|
77
|
+
"criterion": {"type": "selectbox", "options": ["gini", "entropy", "log_loss"]},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
tuner_universal(RandomForestClassifier, param_grid, X, y)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Luego ejecuta:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
streamlit run app.py
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Funcionalidades
|
|
92
|
+
|
|
93
|
+
| Feature | Descripción |
|
|
94
|
+
|---|---|
|
|
95
|
+
| **UI dinámica** | Los controles del sidebar se generan solos desde `param_grid` |
|
|
96
|
+
| **Universalidad** | Compatible con cualquier estimador de la API sklearn |
|
|
97
|
+
| **Fronteras de decisión** | `DecisionBoundaryDisplay` sobre espacio PCA 2D |
|
|
98
|
+
| **Curva ROC** | Binaria o multiclase OvR automática con AUC |
|
|
99
|
+
| **Métricas en vivo** | Accuracy, F1-Score y Recall actualizados en cada interacción |
|
|
100
|
+
| **Exportación** | Genera el bloque de código listo para producción |
|
|
101
|
+
| **Caché** | PCA + split cacheados con `@st.cache_data` para máxima velocidad |
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Esquema de `param_grid`
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
param_grid = {
|
|
109
|
+
# Slider numérico
|
|
110
|
+
"nombre_param": {
|
|
111
|
+
"type": "slider",
|
|
112
|
+
"min": 1,
|
|
113
|
+
"max": 100,
|
|
114
|
+
"default": 10,
|
|
115
|
+
"step": 1, # opcional
|
|
116
|
+
"help": "Tooltip" # opcional
|
|
117
|
+
},
|
|
118
|
+
# Selectbox categórico
|
|
119
|
+
"otro_param": {
|
|
120
|
+
"type": "selectbox",
|
|
121
|
+
"options": ["opcion_a", "opcion_b"],
|
|
122
|
+
"help": "Tooltip" # opcional
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Compatibilidad verificada
|
|
130
|
+
|
|
131
|
+
- `RandomForestClassifier`
|
|
132
|
+
- `GradientBoostingClassifier`
|
|
133
|
+
- `DecisionTreeClassifier`
|
|
134
|
+
- `ExtraTreesClassifier`
|
|
135
|
+
- `XGBClassifier` (con `pip install hypertuner[xgboost]`)
|
|
136
|
+
- `LGBMClassifier` (con `pip install hypertuner[lightgbm]`)
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Licencia
|
|
141
|
+
|
|
142
|
+
MIT © Ceron
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# ⚗ hypertunercer
|
|
2
|
+
|
|
3
|
+
**Playground interactivo de hiperparámetros para cualquier clasificador scikit-learn.**
|
|
4
|
+
|
|
5
|
+
HyperTuner lanza una interfaz Streamlit completa con fronteras de decisión, curvas ROC y métricas en tiempo real — todo generado automáticamente desde un diccionario de configuración.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Instalación
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install hypertunercer
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Con soporte para XGBoost o LightGBM:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install "hypertunercer[xgboost]"
|
|
19
|
+
pip install "hypertunercer[lightgbm]"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Uso rápido
|
|
25
|
+
|
|
26
|
+
Crea un archivo `app.py` con esto:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from hypertunercer import tuner_universal
|
|
30
|
+
from sklearn.ensemble import RandomForestClassifier
|
|
31
|
+
from sklearn.datasets import make_classification
|
|
32
|
+
|
|
33
|
+
X, y = make_classification(n_samples=500, n_features=10, random_state=42)
|
|
34
|
+
|
|
35
|
+
param_grid = {
|
|
36
|
+
"n_estimators": {"type": "slider", "min": 10, "max": 300, "default": 100, "step": 10},
|
|
37
|
+
"max_depth": {"type": "slider", "min": 1, "max": 20, "default": 5},
|
|
38
|
+
"criterion": {"type": "selectbox", "options": ["gini", "entropy", "log_loss"]},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
tuner_universal(RandomForestClassifier, param_grid, X, y)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Luego ejecuta:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
streamlit run app.py
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Funcionalidades
|
|
53
|
+
|
|
54
|
+
| Feature | Descripción |
|
|
55
|
+
|---|---|
|
|
56
|
+
| **UI dinámica** | Los controles del sidebar se generan solos desde `param_grid` |
|
|
57
|
+
| **Universalidad** | Compatible con cualquier estimador de la API sklearn |
|
|
58
|
+
| **Fronteras de decisión** | `DecisionBoundaryDisplay` sobre espacio PCA 2D |
|
|
59
|
+
| **Curva ROC** | Binaria o multiclase OvR automática con AUC |
|
|
60
|
+
| **Métricas en vivo** | Accuracy, F1-Score y Recall actualizados en cada interacción |
|
|
61
|
+
| **Exportación** | Genera el bloque de código listo para producción |
|
|
62
|
+
| **Caché** | PCA + split cacheados con `@st.cache_data` para máxima velocidad |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Esquema de `param_grid`
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
param_grid = {
|
|
70
|
+
# Slider numérico
|
|
71
|
+
"nombre_param": {
|
|
72
|
+
"type": "slider",
|
|
73
|
+
"min": 1,
|
|
74
|
+
"max": 100,
|
|
75
|
+
"default": 10,
|
|
76
|
+
"step": 1, # opcional
|
|
77
|
+
"help": "Tooltip" # opcional
|
|
78
|
+
},
|
|
79
|
+
# Selectbox categórico
|
|
80
|
+
"otro_param": {
|
|
81
|
+
"type": "selectbox",
|
|
82
|
+
"options": ["opcion_a", "opcion_b"],
|
|
83
|
+
"help": "Tooltip" # opcional
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Compatibilidad verificada
|
|
91
|
+
|
|
92
|
+
- `RandomForestClassifier`
|
|
93
|
+
- `GradientBoostingClassifier`
|
|
94
|
+
- `DecisionTreeClassifier`
|
|
95
|
+
- `ExtraTreesClassifier`
|
|
96
|
+
- `XGBClassifier` (con `pip install hypertuner[xgboost]`)
|
|
97
|
+
- `LGBMClassifier` (con `pip install hypertuner[lightgbm]`)
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Licencia
|
|
102
|
+
|
|
103
|
+
MIT © Ceron
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# hypertuner/__init__.py
|
|
2
|
+
# Expone la función pública para que el usuario pueda hacer:
|
|
3
|
+
# from hypertuner import tuner_universal
|
|
4
|
+
|
|
5
|
+
from .tuner import tuner_universal
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
__author__ = "Jesús Alberto Cerón Hernández"
|
|
9
|
+
__all__ = ["tuner_universal"]
|
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# tuner.py — Playground Universal de Hiperparámetros
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Arquitectura: metaprogramación de UI dirigida por datos.
|
|
5
|
+
# La interfaz no tiene controles fijos; se genera a sí misma leyendo el
|
|
6
|
+
# diccionario `param_grid` en tiempo de ejecución. Cada clave del diccionario
|
|
7
|
+
# produce un widget distinto según su campo "type".
|
|
8
|
+
#
|
|
9
|
+
# Uso desde Jupyter o cualquier script externo:
|
|
10
|
+
#
|
|
11
|
+
# from tuner import tuner_universal
|
|
12
|
+
# from sklearn.ensemble import RandomForestClassifier
|
|
13
|
+
#
|
|
14
|
+
# param_grid = {
|
|
15
|
+
# "n_estimators": {"type": "slider", "min": 10, "max": 300,
|
|
16
|
+
# "default": 100, "step": 10},
|
|
17
|
+
# "max_depth": {"type": "slider", "min": 1, "max": 20, "default": 5},
|
|
18
|
+
# "criterion": {"type": "selectbox", "options": ["gini", "entropy"]},
|
|
19
|
+
# }
|
|
20
|
+
# tuner_universal(RandomForestClassifier, param_grid, X, y)
|
|
21
|
+
#
|
|
22
|
+
# =============================================================================
|
|
23
|
+
|
|
24
|
+
import warnings
|
|
25
|
+
warnings.filterwarnings("ignore")
|
|
26
|
+
|
|
27
|
+
import streamlit as st
|
|
28
|
+
import matplotlib
|
|
29
|
+
matplotlib.use("Agg")
|
|
30
|
+
import matplotlib.pyplot as plt
|
|
31
|
+
import matplotlib.colors as mcolors
|
|
32
|
+
import numpy as np
|
|
33
|
+
import pandas as pd
|
|
34
|
+
|
|
35
|
+
from sklearn.decomposition import PCA
|
|
36
|
+
from sklearn.model_selection import train_test_split
|
|
37
|
+
from sklearn.inspection import DecisionBoundaryDisplay
|
|
38
|
+
from sklearn.metrics import (
|
|
39
|
+
accuracy_score, f1_score, recall_score,
|
|
40
|
+
roc_curve, auc, RocCurveDisplay
|
|
41
|
+
)
|
|
42
|
+
from sklearn.preprocessing import label_binarize
|
|
43
|
+
|
|
44
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
# Paleta visual: tema claro — azul marino + blanco, estilo portafolio profesional
|
|
46
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
47
|
+
BG_COLOR = "#F0F4F8" # Fondo gris azulado muy claro
|
|
48
|
+
PANEL_COLOR = "#FFFFFF" # Superficies blancas
|
|
49
|
+
NAVY = "#0D1B3E" # Azul marino oscuro (header/sidebar)
|
|
50
|
+
ACCENT_BLUE = "#3B82F6" # Azul eléctrico para acentos principales
|
|
51
|
+
ACCENT_DARK = "#1E40AF" # Azul profundo para hover y bordes fuertes
|
|
52
|
+
TEXT_COLOR = "#1E293B" # Texto principal casi negro azulado
|
|
53
|
+
MUTED_TEXT = "#64748B" # Texto secundario grisáceo
|
|
54
|
+
GRID_COLOR = "#CBD5E1" # Líneas de grilla sutiles
|
|
55
|
+
SUCCESS_COLOR = "#0369A1" # Azul para métricas positivas
|
|
56
|
+
|
|
57
|
+
# CSS inyectado — tema claro inspirado en el portafolio de Alberto
|
|
58
|
+
CUSTOM_CSS = f"""
|
|
59
|
+
<style>
|
|
60
|
+
@import url('https://fonts.googleapis.com/css2?family=Barlow:wght@300;400;600;700;800&family=Barlow+Condensed:wght@600;700&display=swap');
|
|
61
|
+
|
|
62
|
+
/* ── Reset general ── */
|
|
63
|
+
html, body, [data-testid="stAppViewContainer"] {{
|
|
64
|
+
background-color: {BG_COLOR};
|
|
65
|
+
color: {TEXT_COLOR};
|
|
66
|
+
font-family: 'Barlow', sans-serif;
|
|
67
|
+
}}
|
|
68
|
+
|
|
69
|
+
/* ── Área de contenido principal ── */
|
|
70
|
+
[data-testid="stMain"] {{
|
|
71
|
+
background-color: {BG_COLOR};
|
|
72
|
+
}}
|
|
73
|
+
|
|
74
|
+
/* ── Sidebar: azul marino ── */
|
|
75
|
+
[data-testid="stSidebar"] {{
|
|
76
|
+
background-color: {NAVY};
|
|
77
|
+
border-right: none;
|
|
78
|
+
}}
|
|
79
|
+
/* Texto general del sidebar — solo elementos de texto, no fondos */
|
|
80
|
+
[data-testid="stSidebar"] p,
|
|
81
|
+
[data-testid="stSidebar"] span:not([data-baseweb]) {{
|
|
82
|
+
color: #E2E8F0 !important;
|
|
83
|
+
font-family: 'Barlow', sans-serif !important;
|
|
84
|
+
}}
|
|
85
|
+
/* Labels */
|
|
86
|
+
[data-testid="stSidebar"] label p {{
|
|
87
|
+
color: #93C5FD !important;
|
|
88
|
+
font-size: 12px !important;
|
|
89
|
+
font-weight: 600 !important;
|
|
90
|
+
text-transform: uppercase;
|
|
91
|
+
letter-spacing: 1px;
|
|
92
|
+
}}
|
|
93
|
+
/* Número encima del thumb: fondo transparente, texto blanco */
|
|
94
|
+
[data-testid="stSidebar"] [data-testid="stSliderThumbValue"] {{
|
|
95
|
+
background: transparent !important;
|
|
96
|
+
color: #FFFFFF !important;
|
|
97
|
+
font-weight: 700 !important;
|
|
98
|
+
}}
|
|
99
|
+
/* Selectbox interior arreglado */
|
|
100
|
+
[data-testid="stSidebar"] div[data-baseweb="select"] > div {{
|
|
101
|
+
background-color: #1E3A6E !important;
|
|
102
|
+
border: 1px solid #3B82F6 !important;
|
|
103
|
+
border-radius: 6px !important;
|
|
104
|
+
}}
|
|
105
|
+
[data-testid="stSidebar"] div[data-baseweb="select"] div[class*="singleValue"] {{
|
|
106
|
+
color: #FFFFFF !important;
|
|
107
|
+
font-weight: 600 !important;
|
|
108
|
+
}}
|
|
109
|
+
[data-testid="stSidebar"] ul[role="listbox"] li {{
|
|
110
|
+
color: #FFFFFF !important;
|
|
111
|
+
background-color: #1E3A6E !important;
|
|
112
|
+
}}
|
|
113
|
+
[data-testid="stSidebar"] ul[role="listbox"] li:hover {{
|
|
114
|
+
background-color: #3B82F6 !important;
|
|
115
|
+
}}
|
|
116
|
+
/* ── Header principal ── */
|
|
117
|
+
.tuner-header {{
|
|
118
|
+
font-family: 'Barlow Condensed', sans-serif;
|
|
119
|
+
font-size: 28px;
|
|
120
|
+
font-weight: 700;
|
|
121
|
+
color: {NAVY};
|
|
122
|
+
letter-spacing: 1px;
|
|
123
|
+
text-transform: uppercase;
|
|
124
|
+
border-bottom: 3px solid {ACCENT_BLUE};
|
|
125
|
+
padding-bottom: 12px;
|
|
126
|
+
margin-bottom: 6px;
|
|
127
|
+
}}
|
|
128
|
+
.tuner-subheader {{
|
|
129
|
+
font-family: 'Barlow', sans-serif;
|
|
130
|
+
font-size: 12px;
|
|
131
|
+
font-weight: 400;
|
|
132
|
+
color: {MUTED_TEXT};
|
|
133
|
+
text-transform: uppercase;
|
|
134
|
+
letter-spacing: 2px;
|
|
135
|
+
margin-bottom: 24px;
|
|
136
|
+
}}
|
|
137
|
+
|
|
138
|
+
/* ── Tarjetas de métricas ── */
|
|
139
|
+
[data-testid="stMetric"] {{
|
|
140
|
+
background: {PANEL_COLOR};
|
|
141
|
+
border: 1px solid {GRID_COLOR};
|
|
142
|
+
border-top: 3px solid {ACCENT_BLUE};
|
|
143
|
+
border-radius: 8px;
|
|
144
|
+
padding: 16px;
|
|
145
|
+
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
|
|
146
|
+
}}
|
|
147
|
+
[data-testid="stMetricLabel"] {{
|
|
148
|
+
font-family: 'Barlow', sans-serif !important;
|
|
149
|
+
font-size: 11px !important;
|
|
150
|
+
color: {MUTED_TEXT} !important;
|
|
151
|
+
text-transform: uppercase;
|
|
152
|
+
letter-spacing: 1.5px;
|
|
153
|
+
font-weight: 600 !important;
|
|
154
|
+
}}
|
|
155
|
+
[data-testid="stMetricValue"] {{
|
|
156
|
+
font-family: 'Barlow Condensed', sans-serif !important;
|
|
157
|
+
font-size: 32px !important;
|
|
158
|
+
font-weight: 700 !important;
|
|
159
|
+
color: {NAVY} !important;
|
|
160
|
+
}}
|
|
161
|
+
|
|
162
|
+
/* ── Separador de sección ── */
|
|
163
|
+
.section-label {{
|
|
164
|
+
font-family: 'Barlow', sans-serif;
|
|
165
|
+
font-size: 11px;
|
|
166
|
+
font-weight: 700;
|
|
167
|
+
color: {ACCENT_BLUE};
|
|
168
|
+
text-transform: uppercase;
|
|
169
|
+
letter-spacing: 3px;
|
|
170
|
+
margin: 28px 0 12px 0;
|
|
171
|
+
border-left: 4px solid {ACCENT_BLUE};
|
|
172
|
+
padding-left: 10px;
|
|
173
|
+
}}
|
|
174
|
+
|
|
175
|
+
/* ── Contenedor de gráficas ── */
|
|
176
|
+
[data-testid="stImage"], .stPlotlyChart {{
|
|
177
|
+
background: {PANEL_COLOR};
|
|
178
|
+
border-radius: 8px;
|
|
179
|
+
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
|
|
180
|
+
border: 1px solid {GRID_COLOR};
|
|
181
|
+
}}
|
|
182
|
+
|
|
183
|
+
/* ── Bloque de código exportado ── */
|
|
184
|
+
[data-testid="stCode"] {{
|
|
185
|
+
font-family: 'Barlow Condensed', monospace !important;
|
|
186
|
+
font-size: 13px !important;
|
|
187
|
+
background: #F8FAFC !important;
|
|
188
|
+
border: 1px solid {GRID_COLOR};
|
|
189
|
+
border-left: 4px solid {ACCENT_BLUE};
|
|
190
|
+
border-radius: 6px;
|
|
191
|
+
}}
|
|
192
|
+
|
|
193
|
+
/* ── Botón de exportación en sidebar ── */
|
|
194
|
+
[data-testid="stButton"] > button {{
|
|
195
|
+
background: {ACCENT_BLUE};
|
|
196
|
+
border: none;
|
|
197
|
+
color: #FFFFFF;
|
|
198
|
+
font-family: 'Barlow', sans-serif;
|
|
199
|
+
font-size: 13px;
|
|
200
|
+
font-weight: 700;
|
|
201
|
+
letter-spacing: 1px;
|
|
202
|
+
text-transform: uppercase;
|
|
203
|
+
border-radius: 6px;
|
|
204
|
+
padding: 10px 16px;
|
|
205
|
+
transition: all 0.2s ease;
|
|
206
|
+
width: 100%;
|
|
207
|
+
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.4);
|
|
208
|
+
}}
|
|
209
|
+
[data-testid="stButton"] > button:hover {{
|
|
210
|
+
background: {ACCENT_DARK};
|
|
211
|
+
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.5);
|
|
212
|
+
transform: translateY(-1px);
|
|
213
|
+
}}
|
|
214
|
+
|
|
215
|
+
/* ── Divider ── */
|
|
216
|
+
hr {{
|
|
217
|
+
border-color: {GRID_COLOR};
|
|
218
|
+
margin: 20px 0;
|
|
219
|
+
}}
|
|
220
|
+
|
|
221
|
+
/* ── Alerts/info ── */
|
|
222
|
+
[data-testid="stInfo"] {{
|
|
223
|
+
background: #EFF6FF;
|
|
224
|
+
border-left: 4px solid {ACCENT_BLUE};
|
|
225
|
+
border-radius: 4px;
|
|
226
|
+
color: {NAVY};
|
|
227
|
+
}}
|
|
228
|
+
|
|
229
|
+
/* ── Success alert ── */
|
|
230
|
+
[data-testid="stSuccess"] {{
|
|
231
|
+
background: #F0FDF4;
|
|
232
|
+
border-left: 4px solid #22C55E;
|
|
233
|
+
border-radius: 4px;
|
|
234
|
+
}}
|
|
235
|
+
|
|
236
|
+
/* ── Spinner ── */
|
|
237
|
+
[data-testid="stSpinner"] {{
|
|
238
|
+
color: {ACCENT_BLUE} !important;
|
|
239
|
+
}}
|
|
240
|
+
</style>
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# =============================================================================
|
|
245
|
+
# CAPA DE DATOS — cacheado para evitar re-splits en cada interacción
|
|
246
|
+
# =============================================================================
|
|
247
|
+
|
|
248
|
+
@st.cache_data(show_spinner=False)
|
|
249
|
+
def _preparar_datos(X_array: np.ndarray, y_array: np.ndarray, test_size: float = 0.25):
|
|
250
|
+
"""
|
|
251
|
+
Divide el dataset en train/test y aplica PCA(2D).
|
|
252
|
+
Cacheado por Streamlit: solo se ejecuta una vez por sesión.
|
|
253
|
+
"""
|
|
254
|
+
pca = PCA(n_components=2, random_state=42)
|
|
255
|
+
X_2d = pca.fit_transform(X_array)
|
|
256
|
+
varianza = pca.explained_variance_ratio_.sum()
|
|
257
|
+
|
|
258
|
+
X_train, X_test, y_train, y_test = train_test_split(
|
|
259
|
+
X_2d, y_array, test_size=test_size, random_state=42, stratify=y_array
|
|
260
|
+
)
|
|
261
|
+
return X_train, X_test, y_train, y_test, pca, varianza
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# =============================================================================
|
|
265
|
+
# GENERADOR DINÁMICO DE WIDGETS — metaprogramación dirigida por datos
|
|
266
|
+
# =============================================================================
|
|
267
|
+
|
|
268
|
+
def _generar_sidebar(param_grid: dict) -> dict:
|
|
269
|
+
"""
|
|
270
|
+
Itera sobre param_grid y materializa widgets Streamlit en tiempo real.
|
|
271
|
+
La UI se construye sola leyendo el diccionario — sin controles fijos.
|
|
272
|
+
"""
|
|
273
|
+
kwargs_modelo = {}
|
|
274
|
+
|
|
275
|
+
for nombre_param, config in param_grid.items():
|
|
276
|
+
tipo = config.get("type", "").lower()
|
|
277
|
+
|
|
278
|
+
if tipo == "slider":
|
|
279
|
+
step = config.get("step", None)
|
|
280
|
+
default = config.get("default", config["min"])
|
|
281
|
+
|
|
282
|
+
if any(isinstance(v, float) for v in [config["min"], config["max"], default]):
|
|
283
|
+
step = step if step is not None else 0.01
|
|
284
|
+
else:
|
|
285
|
+
step = step if step is not None else 1
|
|
286
|
+
|
|
287
|
+
valor = st.sidebar.slider(
|
|
288
|
+
label=nombre_param,
|
|
289
|
+
min_value=config["min"],
|
|
290
|
+
max_value=config["max"],
|
|
291
|
+
value=default,
|
|
292
|
+
step=step,
|
|
293
|
+
help=config.get("help", f"Hiperparámetro: {nombre_param}")
|
|
294
|
+
)
|
|
295
|
+
kwargs_modelo[nombre_param] = valor
|
|
296
|
+
|
|
297
|
+
elif tipo == "selectbox":
|
|
298
|
+
opciones = config.get("options", [])
|
|
299
|
+
default_idx = config.get("default_index", 0)
|
|
300
|
+
valor = st.sidebar.selectbox(
|
|
301
|
+
label=nombre_param,
|
|
302
|
+
options=opciones,
|
|
303
|
+
index=default_idx,
|
|
304
|
+
help=config.get("help", f"Hiperparámetro: {nombre_param}")
|
|
305
|
+
)
|
|
306
|
+
kwargs_modelo[nombre_param] = valor
|
|
307
|
+
|
|
308
|
+
else:
|
|
309
|
+
st.sidebar.warning(f"⚠ Tipo desconocido para `{nombre_param}`: '{tipo}'")
|
|
310
|
+
|
|
311
|
+
return kwargs_modelo
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# =============================================================================
|
|
315
|
+
# GRÁFICAS — tema claro, paleta azul marina
|
|
316
|
+
# =============================================================================
|
|
317
|
+
|
|
318
|
+
def _fig_decision_boundary(modelo, X_train, X_test, y_train, y_test, clases):
|
|
319
|
+
"""Fronteras de decisión sobre espacio PCA 2D — tema claro."""
|
|
320
|
+
n_clases = len(clases)
|
|
321
|
+
# Paleta contrastante para tema claro — distinguible sobre fondo blanco
|
|
322
|
+
base_colors = [
|
|
323
|
+
"#2563EB", "#DC2626", "#16A34A", "#D97706",
|
|
324
|
+
"#7C3AED", "#0891B2", "#DB2777", "#65A30D",
|
|
325
|
+
"#EA580C", "#0D9488"
|
|
326
|
+
]
|
|
327
|
+
color_map = {c: base_colors[i % len(base_colors)] for i, c in enumerate(clases)}
|
|
328
|
+
|
|
329
|
+
fig, ax = plt.subplots(figsize=(6.5, 5.2), facecolor=PANEL_COLOR)
|
|
330
|
+
ax.set_facecolor("#F8FAFC")
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
cmap_colors = [mcolors.to_rgba(color_map[c]) for c in clases]
|
|
334
|
+
cmap_discreto = mcolors.LinearSegmentedColormap.from_list(
|
|
335
|
+
"tuner_cmap", cmap_colors, N=n_clases
|
|
336
|
+
)
|
|
337
|
+
DecisionBoundaryDisplay.from_estimator(
|
|
338
|
+
modelo,
|
|
339
|
+
np.vstack([X_train, X_test]),
|
|
340
|
+
ax=ax,
|
|
341
|
+
response_method="predict",
|
|
342
|
+
cmap=cmap_discreto,
|
|
343
|
+
alpha=0.18,
|
|
344
|
+
plot_method="pcolormesh"
|
|
345
|
+
)
|
|
346
|
+
except Exception as e:
|
|
347
|
+
ax.text(0.5, 0.5, f"Fronteras no disponibles\n{str(e)[:60]}",
|
|
348
|
+
ha="center", va="center", color=MUTED_TEXT,
|
|
349
|
+
transform=ax.transAxes, fontsize=9)
|
|
350
|
+
|
|
351
|
+
for clase in clases:
|
|
352
|
+
mask = y_train == clase
|
|
353
|
+
ax.scatter(
|
|
354
|
+
X_train[mask, 0], X_train[mask, 1],
|
|
355
|
+
c=color_map[clase], s=24, alpha=0.7,
|
|
356
|
+
linewidths=0.5, edgecolors=PANEL_COLOR,
|
|
357
|
+
label=f"Train: {clase}"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
for clase in clases:
|
|
361
|
+
mask = y_test == clase
|
|
362
|
+
ax.scatter(
|
|
363
|
+
X_test[mask, 0], X_test[mask, 1],
|
|
364
|
+
c=color_map[clase], s=44, alpha=1.0,
|
|
365
|
+
linewidths=1.4, edgecolors=NAVY,
|
|
366
|
+
marker="^", label=f"Test: {clase}"
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
for spine in ax.spines.values():
|
|
370
|
+
spine.set_edgecolor(GRID_COLOR)
|
|
371
|
+
ax.tick_params(colors=MUTED_TEXT, labelsize=8)
|
|
372
|
+
ax.set_xlabel("PC1", color=MUTED_TEXT, fontsize=9)
|
|
373
|
+
ax.set_ylabel("PC2", color=MUTED_TEXT, fontsize=9)
|
|
374
|
+
ax.set_title("FRONTERAS DE DECISIÓN (espacio PCA)",
|
|
375
|
+
color=NAVY, fontsize=10, fontweight="bold", pad=10)
|
|
376
|
+
ax.grid(color=GRID_COLOR, linewidth=0.5, alpha=0.8)
|
|
377
|
+
ax.legend(loc="upper right", fontsize=7, facecolor=PANEL_COLOR,
|
|
378
|
+
edgecolor=GRID_COLOR, labelcolor=TEXT_COLOR, markerscale=1.2)
|
|
379
|
+
|
|
380
|
+
fig.patch.set_linewidth(1)
|
|
381
|
+
fig.patch.set_edgecolor(GRID_COLOR)
|
|
382
|
+
fig.tight_layout()
|
|
383
|
+
return fig
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _fig_roc(modelo, X_test, y_test, clases):
|
|
387
|
+
"""Curva ROC — tema claro, binaria o multiclase OvR."""
|
|
388
|
+
fig, ax = plt.subplots(figsize=(6.5, 5.2), facecolor=PANEL_COLOR)
|
|
389
|
+
ax.set_facecolor("#F8FAFC")
|
|
390
|
+
n_clases = len(clases)
|
|
391
|
+
|
|
392
|
+
try:
|
|
393
|
+
if n_clases == 2:
|
|
394
|
+
RocCurveDisplay.from_estimator(
|
|
395
|
+
modelo, X_test, y_test, ax=ax,
|
|
396
|
+
color=ACCENT_BLUE, lw=2.5, name="ROC"
|
|
397
|
+
)
|
|
398
|
+
ax.plot([0, 1], [0, 1], linestyle="--",
|
|
399
|
+
color=GRID_COLOR, lw=1.5, label="Azar (AUC=0.50)")
|
|
400
|
+
else:
|
|
401
|
+
y_bin = label_binarize(y_test, classes=clases)
|
|
402
|
+
y_score = modelo.predict_proba(X_test)
|
|
403
|
+
|
|
404
|
+
colores_roc = [
|
|
405
|
+
"#2563EB", "#DC2626", "#16A34A",
|
|
406
|
+
"#D97706", "#7C3AED", "#0891B2"
|
|
407
|
+
]
|
|
408
|
+
|
|
409
|
+
aucs = []
|
|
410
|
+
for i, clase in enumerate(clases):
|
|
411
|
+
fpr, tpr, _ = roc_curve(y_bin[:, i], y_score[:, i])
|
|
412
|
+
roc_auc = auc(fpr, tpr)
|
|
413
|
+
aucs.append(roc_auc)
|
|
414
|
+
ax.plot(fpr, tpr, lw=2,
|
|
415
|
+
color=colores_roc[i % len(colores_roc)],
|
|
416
|
+
label=f"Clase {clase} (AUC={roc_auc:.3f})")
|
|
417
|
+
|
|
418
|
+
all_fpr = np.unique(np.concatenate([
|
|
419
|
+
roc_curve(y_bin[:, i], y_score[:, i])[0]
|
|
420
|
+
for i in range(n_clases)
|
|
421
|
+
]))
|
|
422
|
+
mean_tpr = np.zeros_like(all_fpr)
|
|
423
|
+
for i in range(n_clases):
|
|
424
|
+
fpr_i, tpr_i, _ = roc_curve(y_bin[:, i], y_score[:, i])
|
|
425
|
+
mean_tpr += np.interp(all_fpr, fpr_i, tpr_i)
|
|
426
|
+
mean_tpr /= n_clases
|
|
427
|
+
macro_auc = auc(all_fpr, mean_tpr)
|
|
428
|
+
ax.plot(all_fpr, mean_tpr, lw=2.5, linestyle="--",
|
|
429
|
+
color=TEXT_COLOR,
|
|
430
|
+
label=f"Macro avg (AUC={macro_auc:.3f})")
|
|
431
|
+
ax.plot([0, 1], [0, 1], linestyle=":",
|
|
432
|
+
color=GRID_COLOR, lw=1.5)
|
|
433
|
+
|
|
434
|
+
except Exception as e:
|
|
435
|
+
ax.text(0.5, 0.5, f"ROC no disponible\n{str(e)[:80]}",
|
|
436
|
+
ha="center", va="center", color=MUTED_TEXT,
|
|
437
|
+
transform=ax.transAxes, fontsize=9)
|
|
438
|
+
|
|
439
|
+
for spine in ax.spines.values():
|
|
440
|
+
spine.set_edgecolor(GRID_COLOR)
|
|
441
|
+
ax.tick_params(colors=MUTED_TEXT, labelsize=8)
|
|
442
|
+
ax.set_xlabel("Tasa de Falsos Positivos", color=MUTED_TEXT, fontsize=9)
|
|
443
|
+
ax.set_ylabel("Tasa de Verdaderos Positivos", color=MUTED_TEXT, fontsize=9)
|
|
444
|
+
ax.set_title("CURVA ROC", color=NAVY, fontsize=10,
|
|
445
|
+
fontweight="bold", pad=10)
|
|
446
|
+
ax.grid(color=GRID_COLOR, linewidth=0.5, alpha=0.8)
|
|
447
|
+
ax.set_xlim([-0.02, 1.02])
|
|
448
|
+
ax.set_ylim([-0.02, 1.05])
|
|
449
|
+
ax.legend(loc="lower right", fontsize=7.5, facecolor=PANEL_COLOR,
|
|
450
|
+
edgecolor=GRID_COLOR, labelcolor=TEXT_COLOR)
|
|
451
|
+
|
|
452
|
+
fig.patch.set_linewidth(1)
|
|
453
|
+
fig.patch.set_edgecolor(GRID_COLOR)
|
|
454
|
+
fig.tight_layout()
|
|
455
|
+
return fig
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
# =============================================================================
|
|
459
|
+
# FUNCIÓN PRINCIPAL PÚBLICA
|
|
460
|
+
# =============================================================================
|
|
461
|
+
|
|
462
|
+
def tuner_universal(modelo_clase, param_grid: dict, X, y):
|
|
463
|
+
"""
|
|
464
|
+
Lanza el Playground Interactivo de Hiperparámetros en Streamlit.
|
|
465
|
+
|
|
466
|
+
Parameters
|
|
467
|
+
----------
|
|
468
|
+
modelo_clase : clase sklearn (no instancia)
|
|
469
|
+
param_grid : dict de especificación de hiperparámetros
|
|
470
|
+
X : features (array-like o DataFrame)
|
|
471
|
+
y : etiquetas de clase
|
|
472
|
+
"""
|
|
473
|
+
# ── Configuración de página ──────────────────────────────────────────────
|
|
474
|
+
st.set_page_config(
|
|
475
|
+
page_title="HyperTuner — ML Playground",
|
|
476
|
+
page_icon="⚗",
|
|
477
|
+
layout="wide",
|
|
478
|
+
initial_sidebar_state="expanded"
|
|
479
|
+
)
|
|
480
|
+
st.markdown(CUSTOM_CSS, unsafe_allow_html=True)
|
|
481
|
+
|
|
482
|
+
# ── Header ───────────────────────────────────────────────────────────────
|
|
483
|
+
nombre_modelo = modelo_clase.__name__
|
|
484
|
+
st.markdown(
|
|
485
|
+
f'<div class="tuner-header">⚗ HyperTuner // {nombre_modelo}</div>',
|
|
486
|
+
unsafe_allow_html=True
|
|
487
|
+
)
|
|
488
|
+
st.markdown(
|
|
489
|
+
'<div class="tuner-subheader">'
|
|
490
|
+
'Playground Interactivo de Hiperparámetros · Espacio PCA 2D'
|
|
491
|
+
'</div>',
|
|
492
|
+
unsafe_allow_html=True
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
# ── Sidebar ──────────────────────────────────────────────────────────────
|
|
496
|
+
st.sidebar.markdown(
|
|
497
|
+
f'<div style="font-family:Barlow,sans-serif; font-size:18px; '
|
|
498
|
+
f'font-weight:800; color:#FFFFFF; text-transform:uppercase; '
|
|
499
|
+
f'letter-spacing:2px; margin-bottom:4px;">{nombre_modelo}</div>'
|
|
500
|
+
f'<div style="font-size:10px; color:#93C5FD; letter-spacing:1px; '
|
|
501
|
+
f'margin-bottom:20px;">HIPERPARÁMETROS</div>',
|
|
502
|
+
unsafe_allow_html=True
|
|
503
|
+
)
|
|
504
|
+
st.sidebar.markdown(
|
|
505
|
+
'<hr style="border-color:#1E3A6E; margin:0 0 16px 0;">',
|
|
506
|
+
unsafe_allow_html=True
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# Metaprogramación: UI generada desde param_grid
|
|
510
|
+
kwargs_modelo = _generar_sidebar(param_grid)
|
|
511
|
+
|
|
512
|
+
st.sidebar.markdown(
|
|
513
|
+
'<hr style="border-color:#1E3A6E; margin:16px 0;">',
|
|
514
|
+
unsafe_allow_html=True
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
# ── Datos (cacheados) ────────────────────────────────────────────────────
|
|
518
|
+
X_arr = np.array(X)
|
|
519
|
+
y_arr = np.array(y)
|
|
520
|
+
|
|
521
|
+
try:
|
|
522
|
+
X_train, X_test, y_train, y_test, pca, varianza = _preparar_datos(X_arr, y_arr)
|
|
523
|
+
except ValueError as e:
|
|
524
|
+
st.error(f"Error al preparar los datos: {e}")
|
|
525
|
+
st.stop()
|
|
526
|
+
|
|
527
|
+
clases = np.unique(y_arr)
|
|
528
|
+
|
|
529
|
+
# ── Instanciar y entrenar el modelo ──────────────────────────────────────
|
|
530
|
+
try:
|
|
531
|
+
modelo = modelo_clase(**kwargs_modelo, random_state=42)
|
|
532
|
+
except TypeError:
|
|
533
|
+
try:
|
|
534
|
+
modelo = modelo_clase(**kwargs_modelo)
|
|
535
|
+
except Exception as e:
|
|
536
|
+
st.error(f"Error al instanciar `{nombre_modelo}`: {e}")
|
|
537
|
+
st.stop()
|
|
538
|
+
|
|
539
|
+
try:
|
|
540
|
+
modelo.fit(X_train, y_train)
|
|
541
|
+
y_pred = modelo.predict(X_test)
|
|
542
|
+
except Exception as e:
|
|
543
|
+
st.error(f"Error durante el entrenamiento: {e}")
|
|
544
|
+
st.stop()
|
|
545
|
+
|
|
546
|
+
# ── Métricas ─────────────────────────────────────────────────────────────
|
|
547
|
+
avg = "binary" if len(clases) == 2 else "weighted"
|
|
548
|
+
accuracy = accuracy_score(y_test, y_pred)
|
|
549
|
+
f1 = f1_score(y_test, y_pred, average=avg, zero_division=0)
|
|
550
|
+
recall = recall_score(y_test, y_pred, average=avg, zero_division=0)
|
|
551
|
+
|
|
552
|
+
st.markdown('<div class="section-label">Métricas en tiempo real</div>',
|
|
553
|
+
unsafe_allow_html=True)
|
|
554
|
+
|
|
555
|
+
col_acc, col_f1, col_rec, col_pca = st.columns([1, 1, 1, 1.4])
|
|
556
|
+
with col_acc:
|
|
557
|
+
st.metric("ACCURACY", f"{accuracy:.4f}")
|
|
558
|
+
with col_f1:
|
|
559
|
+
st.metric(f"F1-SCORE ({avg})", f"{f1:.4f}")
|
|
560
|
+
with col_rec:
|
|
561
|
+
st.metric(f"RECALL ({avg})", f"{recall:.4f}")
|
|
562
|
+
with col_pca:
|
|
563
|
+
st.metric("VARIANZA EXPLICADA (PCA 2D)", f"{varianza:.2%}",
|
|
564
|
+
help="Información original conservada en 2 dimensiones")
|
|
565
|
+
|
|
566
|
+
st.markdown("<br>", unsafe_allow_html=True)
|
|
567
|
+
|
|
568
|
+
# ── Visualizaciones ──────────────────────────────────────────────────────
|
|
569
|
+
st.markdown('<div class="section-label">Visualizaciones</div>',
|
|
570
|
+
unsafe_allow_html=True)
|
|
571
|
+
|
|
572
|
+
col_db, col_roc = st.columns(2, gap="medium")
|
|
573
|
+
|
|
574
|
+
with col_db:
|
|
575
|
+
with st.spinner("Calculando fronteras..."):
|
|
576
|
+
fig_db = _fig_decision_boundary(
|
|
577
|
+
modelo, X_train, X_test, y_train, y_test, clases
|
|
578
|
+
)
|
|
579
|
+
st.pyplot(fig_db, use_container_width=True)
|
|
580
|
+
plt.close(fig_db)
|
|
581
|
+
|
|
582
|
+
with col_roc:
|
|
583
|
+
with st.spinner("Calculando ROC..."):
|
|
584
|
+
fig_roc = _fig_roc(modelo, X_test, y_test, clases)
|
|
585
|
+
st.pyplot(fig_roc, use_container_width=True)
|
|
586
|
+
plt.close(fig_roc)
|
|
587
|
+
|
|
588
|
+
# ── Exportar configuración ───────────────────────────────────────────────
|
|
589
|
+
if st.sidebar.button("⬡ EXPORTAR CONFIGURACIÓN"):
|
|
590
|
+
st.markdown('<div class="section-label">Configuración exportada</div>',
|
|
591
|
+
unsafe_allow_html=True)
|
|
592
|
+
lineas_params = ",\n ".join(
|
|
593
|
+
f'"{k}": {repr(v)}' for k, v in kwargs_modelo.items()
|
|
594
|
+
)
|
|
595
|
+
codigo_exportado = (
|
|
596
|
+
f"# Hiperparámetros seleccionados — {nombre_modelo}\n"
|
|
597
|
+
f"# Generado por HyperTuner\n\n"
|
|
598
|
+
f"from {modelo_clase.__module__} import {nombre_modelo}\n\n"
|
|
599
|
+
f"kwargs_modelo = {{\n"
|
|
600
|
+
f" {lineas_params}\n"
|
|
601
|
+
f"}}\n\n"
|
|
602
|
+
f"modelo = {nombre_modelo}(**kwargs_modelo)\n"
|
|
603
|
+
f"modelo.fit(X_train, y_train)"
|
|
604
|
+
)
|
|
605
|
+
st.code(codigo_exportado, language="python")
|
|
606
|
+
st.success("✓ Copia el bloque anterior en tu pipeline de producción.")
|
|
607
|
+
|
|
608
|
+
# ── Footer ───────────────────────────────────────────────────────────────
|
|
609
|
+
st.markdown("---")
|
|
610
|
+
st.markdown(
|
|
611
|
+
f'<div style="font-family:Barlow,sans-serif; font-size:11px; '
|
|
612
|
+
f'color:{MUTED_TEXT}; text-align:center;">'
|
|
613
|
+
f'HyperTuner · {nombre_modelo} · '
|
|
614
|
+
f'{X_arr.shape[0]:,} muestras · {X_arr.shape[1]} features → PCA 2D · '
|
|
615
|
+
f'Train 75% / Test 25%'
|
|
616
|
+
f'</div>',
|
|
617
|
+
unsafe_allow_html=True
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
# =============================================================================
|
|
622
|
+
# BLOQUE DEMO
|
|
623
|
+
# =============================================================================
|
|
624
|
+
|
|
625
|
+
if __name__ == "__main__":
|
|
626
|
+
from sklearn.ensemble import RandomForestClassifier
|
|
627
|
+
from sklearn.datasets import make_classification
|
|
628
|
+
|
|
629
|
+
X_demo, y_demo = make_classification(
|
|
630
|
+
n_samples=400, n_features=8, n_informative=5,
|
|
631
|
+
n_classes=2, class_sep=1.2, random_state=0
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
PARAM_GRID_RF = {
|
|
635
|
+
"n_estimators": {"type": "slider", "min": 10, "max": 500, "default": 100, "step": 10},
|
|
636
|
+
"max_depth": {"type": "slider", "min": 1, "max": 30, "default": 5},
|
|
637
|
+
"min_samples_split": {"type": "slider", "min": 2, "max": 20, "default": 2},
|
|
638
|
+
"min_samples_leaf": {"type": "slider", "min": 1, "max": 15, "default": 1},
|
|
639
|
+
"criterion": {"type": "selectbox", "options": ["gini", "entropy", "log_loss"]},
|
|
640
|
+
"max_features": {"type": "selectbox", "options": ["sqrt", "log2", None]},
|
|
641
|
+
"bootstrap": {"type": "selectbox", "options": [True, False]},
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
tuner_universal(RandomForestClassifier, PARAM_GRID_RF, X_demo, y_demo)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hypertunercer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Playground interactivo de hiperparámetros para modelos sklearn en Streamlit
|
|
5
|
+
Author: Jesús Alberto Cerón Hernández
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/tu-ceronbuu/hypertuner
|
|
8
|
+
Project-URL: Repository, https://github.com/tu-usuario/hypertuner
|
|
9
|
+
Project-URL: Issues, https://github.com/tu-usuario/hypertuner/issues
|
|
10
|
+
Keywords: machine-learning,hyperparameter-tuning,streamlit,scikit-learn,visualization,interactive
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: streamlit>=1.30
|
|
25
|
+
Requires-Dist: scikit-learn>=1.3
|
|
26
|
+
Requires-Dist: matplotlib>=3.7
|
|
27
|
+
Requires-Dist: pandas>=2.0
|
|
28
|
+
Requires-Dist: numpy>=1.24
|
|
29
|
+
Provides-Extra: xgboost
|
|
30
|
+
Requires-Dist: xgboost>=2.0; extra == "xgboost"
|
|
31
|
+
Provides-Extra: lightgbm
|
|
32
|
+
Requires-Dist: lightgbm>=4.0; extra == "lightgbm"
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: pytest; extra == "dev"
|
|
35
|
+
Requires-Dist: black; extra == "dev"
|
|
36
|
+
Requires-Dist: ruff; extra == "dev"
|
|
37
|
+
Requires-Dist: build; extra == "dev"
|
|
38
|
+
Requires-Dist: twine; extra == "dev"
|
|
39
|
+
|
|
40
|
+
# ⚗ hypertunercer
|
|
41
|
+
|
|
42
|
+
**Playground interactivo de hiperparámetros para cualquier clasificador scikit-learn.**
|
|
43
|
+
|
|
44
|
+
HyperTuner lanza una interfaz Streamlit completa con fronteras de decisión, curvas ROC y métricas en tiempo real — todo generado automáticamente desde un diccionario de configuración.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Instalación
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install hypertunercer
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Con soporte para XGBoost o LightGBM:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install "hypertunercer[xgboost]"
|
|
58
|
+
pip install "hypertunercer[lightgbm]"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Uso rápido
|
|
64
|
+
|
|
65
|
+
Crea un archivo `app.py` con esto:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from hypertunercer import tuner_universal
|
|
69
|
+
from sklearn.ensemble import RandomForestClassifier
|
|
70
|
+
from sklearn.datasets import make_classification
|
|
71
|
+
|
|
72
|
+
X, y = make_classification(n_samples=500, n_features=10, random_state=42)
|
|
73
|
+
|
|
74
|
+
param_grid = {
|
|
75
|
+
"n_estimators": {"type": "slider", "min": 10, "max": 300, "default": 100, "step": 10},
|
|
76
|
+
"max_depth": {"type": "slider", "min": 1, "max": 20, "default": 5},
|
|
77
|
+
"criterion": {"type": "selectbox", "options": ["gini", "entropy", "log_loss"]},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
tuner_universal(RandomForestClassifier, param_grid, X, y)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Luego ejecuta:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
streamlit run app.py
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Funcionalidades
|
|
92
|
+
|
|
93
|
+
| Feature | Descripción |
|
|
94
|
+
|---|---|
|
|
95
|
+
| **UI dinámica** | Los controles del sidebar se generan solos desde `param_grid` |
|
|
96
|
+
| **Universalidad** | Compatible con cualquier estimador de la API sklearn |
|
|
97
|
+
| **Fronteras de decisión** | `DecisionBoundaryDisplay` sobre espacio PCA 2D |
|
|
98
|
+
| **Curva ROC** | Binaria o multiclase OvR automática con AUC |
|
|
99
|
+
| **Métricas en vivo** | Accuracy, F1-Score y Recall actualizados en cada interacción |
|
|
100
|
+
| **Exportación** | Genera el bloque de código listo para producción |
|
|
101
|
+
| **Caché** | PCA + split cacheados con `@st.cache_data` para máxima velocidad |
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Esquema de `param_grid`
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
param_grid = {
|
|
109
|
+
# Slider numérico
|
|
110
|
+
"nombre_param": {
|
|
111
|
+
"type": "slider",
|
|
112
|
+
"min": 1,
|
|
113
|
+
"max": 100,
|
|
114
|
+
"default": 10,
|
|
115
|
+
"step": 1, # opcional
|
|
116
|
+
"help": "Tooltip" # opcional
|
|
117
|
+
},
|
|
118
|
+
# Selectbox categórico
|
|
119
|
+
"otro_param": {
|
|
120
|
+
"type": "selectbox",
|
|
121
|
+
"options": ["opcion_a", "opcion_b"],
|
|
122
|
+
"help": "Tooltip" # opcional
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Compatibilidad verificada
|
|
130
|
+
|
|
131
|
+
- `RandomForestClassifier`
|
|
132
|
+
- `GradientBoostingClassifier`
|
|
133
|
+
- `DecisionTreeClassifier`
|
|
134
|
+
- `ExtraTreesClassifier`
|
|
135
|
+
- `XGBClassifier` (con `pip install hypertuner[xgboost]`)
|
|
136
|
+
- `LGBMClassifier` (con `pip install hypertuner[lightgbm]`)
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Licencia
|
|
141
|
+
|
|
142
|
+
MIT © Ceron
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
hypertuner/__init__.py
|
|
4
|
+
hypertuner/tuner.py
|
|
5
|
+
hypertunercer.egg-info/PKG-INFO
|
|
6
|
+
hypertunercer.egg-info/SOURCES.txt
|
|
7
|
+
hypertunercer.egg-info/dependency_links.txt
|
|
8
|
+
hypertunercer.egg-info/requires.txt
|
|
9
|
+
hypertunercer.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hypertuner
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hypertunercer" # nombre en PyPI — cámbialo si ya está tomado
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Playground interactivo de hiperparámetros para modelos sklearn en Streamlit"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Jesús Alberto Cerón Hernández" }
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
keywords = [
|
|
18
|
+
"machine-learning", "hyperparameter-tuning", "streamlit",
|
|
19
|
+
"scikit-learn", "visualization", "interactive"
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Development Status :: 3 - Alpha",
|
|
24
|
+
"Intended Audience :: Science/Research",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.9",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
33
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# Dependencias mínimas para que el paquete funcione
|
|
37
|
+
dependencies = [
|
|
38
|
+
"streamlit>=1.30",
|
|
39
|
+
"scikit-learn>=1.3",
|
|
40
|
+
"matplotlib>=3.7",
|
|
41
|
+
"pandas>=2.0",
|
|
42
|
+
"numpy>=1.24",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[project.optional-dependencies]
|
|
46
|
+
# Extras opcionales: el usuario instala con pip install hypertuner[xgboost]
|
|
47
|
+
xgboost = ["xgboost>=2.0"]
|
|
48
|
+
lightgbm = ["lightgbm>=4.0"]
|
|
49
|
+
dev = ["pytest", "black", "ruff", "build", "twine"]
|
|
50
|
+
|
|
51
|
+
[project.urls]
|
|
52
|
+
Homepage = "https://github.com/tu-ceronbuu/hypertuner"
|
|
53
|
+
Repository = "https://github.com/tu-usuario/hypertuner"
|
|
54
|
+
Issues = "https://github.com/tu-usuario/hypertuner/issues"
|
|
55
|
+
|
|
56
|
+
[tool.setuptools.packages.find]
|
|
57
|
+
where = ["."] # busca paquetes desde la raíz del proyecto
|
|
58
|
+
include = ["hypertuner*"]
|
|
59
|
+
|
|
60
|
+
[tool.setuptools.package-data]
|
|
61
|
+
hypertuner = ["py.typed"] # marca el paquete como type-aware (PEP 561)
|