feat: horizonte de 35 dias y histograma real en /perfil

El cambio de perfil funcionaba, pero con 13 dias planificados por delante la
diferencia entre perfiles no se apreciaba: la variedad es una propiedad
estadistica y con esa muestra un 51% y un 29% se ven casi igual.

- Horizonte de 21 a 35 dias naturales (~22 laborables). Con esa muestra el
  histograma ya se corresponde con lo anunciado: disperso da 6 horas distintas
  con la mas repetida al 27%, frente al 29% medido sobre el año entero.

- /perfil enseña ahora el histograma REAL de lo que hay sorteado, no solo las
  cifras teoricas del perfil. Asi se ve el efecto del cambio en el momento, que
  era justo lo que faltaba para poder decidir.

Verificado que los cinco perfiles siguen dando exactamente las cifras
documentadas tras el refactor a perfiles por tramo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-24 19:55:13 +02:00
parent a1eef6472f
commit ba0d6b5f08
2 changed files with 23 additions and 1 deletions

View File

@@ -291,8 +291,30 @@ class BotUI:
lines.append(f" <i>{profiles.resumen(n)}</i>") lines.append(f" <i>{profiles.resumen(n)}</i>")
lines.append("\n<i>madrugón = de esos minutos, los que te levantas antes " lines.append("\n<i>madrugón = de esos minutos, los que te levantas antes "
"por la entrada de la mañana. Tu histórico real: 4 minutos, moda 38%.</i>") "por la entrada de la mañana. Tu histórico real: 4 minutos, moda 38%.</i>")
real = self._histograma_real()
if real:
lines.append("\n" + real)
return "\n".join(lines) return "\n".join(lines)
def _histograma_real(self) -> str:
"""Lo que hay sorteado ahora mismo, para poder ver el efecto del cambio."""
from collections import Counter
hoy = self.svc.today()
c: Counter = Counter()
for i in range(60):
for r in self.svc.store.get_plan(hoy + timedelta(days=i)):
t = datetime.fromisoformat(r["target"])
if r["typ"] == ENTRADA and (t.hour, t.minute) == (9, 0):
c[datetime.fromisoformat(r["planned_at"]).strftime("%H:%M")] += 1
if not c:
return ""
tot = sum(c.values())
filas = "\n".join(
f" <code>{h}</code> {'█' * n} {n}" for h, n in sorted(c.items()))
return (f"<b>Tus próximos {tot} días ya sorteados</b> (entrada de la mañana)\n"
f"{filas}\n <i>{len(c)} horas distintas · la más repetida "
f"{max(c.values())} de {tot} ({round(100 * max(c.values()) / tot)}%)</i>")
def _perfil_teclado(self) -> InlineKeyboardMarkup: def _perfil_teclado(self) -> InlineKeyboardMarkup:
actual = self.svc.store.get("perfil", profiles.DEFECTO) actual = self.svc.store.get("perfil", profiles.DEFECTO)
botones = [InlineKeyboardButton( botones = [InlineKeyboardButton(

View File

@@ -27,7 +27,7 @@ logger = logging.getLogger(__name__)
ANNOUNCE_LEAD = timedelta(minutes=12) # cuanta antelacion tiene el killswitch ANNOUNCE_LEAD = timedelta(minutes=12) # cuanta antelacion tiene el killswitch
MAX_SLEEP = 60.0 MAX_SLEEP = 60.0
REPLAN_HOUR = 4 # hora a la que se planifica el dia REPLAN_HOUR = 4 # hora a la que se planifica el dia
HORIZON_DAYS = 21 # dias que se planifican por adelantado HORIZON_DAYS = 35 # dias que se planifican por adelantado
class Service: class Service: