fix: el jitter era demasiado regular; planificar por adelantado
El 96% de las entradas caian en solo dos minutos (08:58 / 08:59). El historial
real del usuario tiene 4 valores repartidos, o sea que el bot resultaba MAS
regular que una persona.
Tres cosas estaban mal:
- La deriva de la costumbre se aplicaba SUMANDO segundos. Cuando salia negativa
hundia el valor contra el suelo y amontonaba medio año en el mismo minuto,
justo lo contrario de lo que pretendia. Ahora va en escala logaritmica:
multiplica el centro en vez de sumarse. Solo con eso la moda baja del 50% al
39%, sin coste ninguno.
- La distribucion diaria era uniforme. Una lognormal da mas minutos distintos
por menos presencia extra, porque la mayoria de los dias caen baratos cerca de
la hora y solo unos pocos se van lejos. Ademas es la forma real de la llegada
de alguien que intenta ser puntual.
- El tope recortaba con min(), asi que un 3% de los dias caia exactamente en el
limite y se veia como un muro en el histograma. Ahora resortea.
El compromiso de fondo no tiene truco: solo se puede variar hacia atras, y
alejarse cuesta presencia. Perfiles medidos sobre los 226 dias laborables del
año, los tres con saldo 0:
discreto 2,8 min/dia entrada 2 minutos, moda 75%
natural 4,2 min/dia entrada 5 minutos, moda 51% <- por defecto
camuflado 5,5 min/dia entrada 4 minutos, moda 43%
Ademas, /proximos mostraba una banda estimada que ocultaba la cola. Ahora se
planifican 21 dias por adelantado y se muestran las horas exactas, con lo que
la prevision es real y se puede decidir sobre ella. El plan guardado se
revalida contra el calendario de Timenet y se regenera si cambia el tipo de
jornada de un dia. Google Calendar se consulta con una sola autenticacion para
todo el horizonte.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -76,23 +76,21 @@ class Planner:
|
||||
start = datetime.combine(day, datetime.min.time(), self.tz) + timedelta(minutes=w.start)
|
||||
end = datetime.combine(day, datetime.min.time(), self.tz) + timedelta(minutes=w.end)
|
||||
|
||||
# ENTRADA: siempre antes de la hora teorica
|
||||
# ENTRADA: siempre antes de la hora teorica. El tope se acota ademas
|
||||
# con el margen de entrada que declara el propio tipo de jornada.
|
||||
tope = min(self.j["entry_cap"], max(w.entry_margin * 60 // 2, 60)) if w.entry_margin \
|
||||
else self.j["entry_cap"]
|
||||
off = jitter.offset_seconds(
|
||||
self.j["entry_before_min"], self.j["entry_before_max"],
|
||||
self._drift(wi, ENTRADA, rng), rng,
|
||||
# el margen de entrada del propio Timenet acota cuanto se puede adelantar
|
||||
outlier_cap=min(self.j["entry_before_max"] * 3, max(w.entry_margin * 60 // 2, 60)),
|
||||
)
|
||||
self.j["entry_floor"], self.j["entry_median"], self.j["entry_sigma"],
|
||||
tope, self._drift(wi, ENTRADA, rng), rng)
|
||||
off = max(off, self.j["entry_guard"])
|
||||
out.append(PlannedCheck(idx, ENTRADA, wi, start, start - timedelta(seconds=off)))
|
||||
idx += 1
|
||||
|
||||
# SALIDA: siempre despues
|
||||
off = jitter.offset_seconds(
|
||||
self.j["exit_after_min"], self.j["exit_after_max"],
|
||||
self._drift(wi, SALIDA, rng), rng,
|
||||
outlier_cap=self.j["exit_after_max"] * 4,
|
||||
)
|
||||
self.j["exit_floor"], self.j["exit_median"], self.j["exit_sigma"],
|
||||
self.j["exit_cap"], self._drift(wi, SALIDA, rng), rng)
|
||||
out.append(PlannedCheck(idx, SALIDA, wi, end, end + timedelta(seconds=off)))
|
||||
idx += 1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user