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.
@@ -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 &nbsp;·&nbsp; {nombre_modelo} &nbsp;·&nbsp; '
614
+ f'{X_arr.shape[0]:,} muestras &nbsp;·&nbsp; {X_arr.shape[1]} features → PCA 2D &nbsp;·&nbsp; '
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,18 @@
1
+ streamlit>=1.30
2
+ scikit-learn>=1.3
3
+ matplotlib>=3.7
4
+ pandas>=2.0
5
+ numpy>=1.24
6
+
7
+ [dev]
8
+ pytest
9
+ black
10
+ ruff
11
+ build
12
+ twine
13
+
14
+ [lightgbm]
15
+ lightgbm>=4.0
16
+
17
+ [xgboost]
18
+ xgboost>=2.0
@@ -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)
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+