feat: perfiles de jitter intercambiables desde Telegram
Cinco perfiles con distinto punto en el compromiso entre variedad visible y
minutos de presencia de mas. Todos dan saldo 0. Se cambian en caliente con
/perfil, con botones, y al cambiar se vuelven a tirar los dados de todos los
dias del horizonte que no se hayan fichado ya.
min/dia madrugon entrada 09:00
discreto 2,8 1,3 2 minutos, moda 75%
natural 4,1 1,8 5 minutos, moda 51% <- por defecto
camuflado 5,5 2,1 4 minutos, moda 43%
disperso 7,9 3,0 6 minutos, moda 29%
sin_madrugar 5,7 1,3 2 minutos, moda 77%
(historico real del usuario: 4 minutos, moda 38%)
La distincion util no es solo el coste total sino DONDE cae. Adelantar la
entrada de la mañana alarga el dia; adelantar la vuelta de comer solo acorta la
comida y se sale a la misma hora. De ahi "sin_madrugar": entrada de la mañana lo
mas pegada posible a las 9 (madrugon 1,26 min/dia, el mas bajo de todos, por
debajo incluso de "discreto") y toda la variedad en la pausa y las salidas.
Para eso los perfiles admiten afinar por tramo con sufijo _w<n>:
entry_median_w0 gana a entry_median para el tramo de la mañana.
tools/perfiles.py mide cada perfil sobre los 226 dias laborables del calendario
real; las cifras de arriba salen de ahi y van guardadas como metadatos para que
/perfil las muestre sin recalcular.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
86
tools/perfiles.py
Normal file
86
tools/perfiles.py
Normal file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Mide cada perfil de jitter sobre el calendario real del año. No ficha nada."""
|
||||
import json, os, random, sys, tempfile
|
||||
from collections import Counter
|
||||
from datetime import timedelta
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from app import profiles, schedule, settings
|
||||
from app.planner import ENTRADA, Planner
|
||||
from app.store import Store
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
CAL = os.path.join(ROOT, "data", "calendar_cache.json")
|
||||
|
||||
|
||||
def medir(nombre, cal, cfg, semilla=11):
|
||||
"""Devuelve (saldo, presencia_extra_min_dia, histograma_entrada, histograma_salida)."""
|
||||
st = Store(os.path.join(tempfile.mkdtemp(), "m.db"))
|
||||
st.put("perfil", nombre)
|
||||
pl = Planner(cfg, st)
|
||||
rng = random.Random(semilla)
|
||||
lo, hi = cal.span
|
||||
ent, sal = Counter(), Counter()
|
||||
P = W = pres = dias = 0
|
||||
peor = 10 ** 9
|
||||
madrugon = 0 # minutos de mas en la ENTRADA DE LA MAÑANA (la que alarga el dia)
|
||||
d = lo
|
||||
while d <= hi:
|
||||
if cal.is_workday(d):
|
||||
plan = pl.plan_day(cal, d, rng)
|
||||
dias += 1
|
||||
for wi, w in enumerate(cal.windows(d)):
|
||||
e = [x for x in plan if x.window_idx == wi and x.typ == ENTRADA][0]
|
||||
s = [x for x in plan if x.window_idx == wi and x.typ != ENTRADA][0]
|
||||
em = e.planned_at.hour * 60 + e.planned_at.minute
|
||||
sm = s.planned_at.hour * 60 + s.planned_at.minute
|
||||
W += min(sm, w.end) - max(em, w.start)
|
||||
P += w.minutes
|
||||
pres += sm - em
|
||||
if wi == 0:
|
||||
madrugon += w.start - em
|
||||
if w.start == 540:
|
||||
ent[e.planned_at.strftime("%H:%M")] += 1
|
||||
if w.end == 1110:
|
||||
sal[s.planned_at.strftime("%H:%M")] += 1
|
||||
peor = min(peor, min(x.offset_seconds for x in plan))
|
||||
d += timedelta(days=1)
|
||||
st.close()
|
||||
return dict(saldo=W - P, coste=(pres - P) / dias, madrugon=madrugon / dias,
|
||||
peor=peor, ent=ent, sal=sal, dias=dias)
|
||||
|
||||
|
||||
def barra(c, ancho=40):
|
||||
t = sum(c.values())
|
||||
vis = {m: n / t for m, n in sorted(c.items()) if n / t >= 0.03}
|
||||
return vis, " ".join("%s %d%%" % (m, round(100 * p)) for m, p in vis.items())
|
||||
|
||||
|
||||
def main():
|
||||
cfg = settings.load()
|
||||
cal = schedule.Calendar(json.load(open(CAL)))
|
||||
print("%-14s %6s %9s %10s %6s %6s" %
|
||||
("perfil", "saldo", "min/día", "madrugón", "mins", "moda"))
|
||||
print("-" * 62)
|
||||
for n in profiles.nombres():
|
||||
r = medir(n, cal, cfg)
|
||||
ve, _ = barra(r["ent"])
|
||||
marca = " ←" if n == profiles.DEFECTO else ""
|
||||
print("%-14s %+5d %8.2f %9.2f %6d %5d%%%s" %
|
||||
(n, r["saldo"], r["coste"], r["madrugon"], len(ve),
|
||||
round(100 * max(ve.values())), marca))
|
||||
print("\nmadrugón = minutos/día que te levantas antes por la entrada de la mañana")
|
||||
print("min/día = presencia total de más (mañana + comida + salidas)\n")
|
||||
|
||||
for n in profiles.nombres():
|
||||
r = medir(n, cal, cfg)
|
||||
_, se = barra(r["ent"])
|
||||
_, ss = barra(r["sal"])
|
||||
print("=== %s === %s" % (profiles.titulo(n), profiles.resumen(n)))
|
||||
print(" entrada 09:00 %s" % se)
|
||||
print(" salida 18:30 %s" % ss)
|
||||
print("tu histórico real: entrada 09:00 23% 09:01 8% 09:02 31% 09:03 38%")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user