Simplifique su programación compleja con timeboard, una biblioteca de Python
por Maxim Mamaev
timeboard
es una biblioteca de Python que crea horarios de períodos de trabajo y realiza cálculos de calendario sobre ellos. Puede crear calendarios de días hábiles estándar, así como una variedad de otros horarios, simples o complejos.
Puedes encontrar la documentación aquí.
Consulta el repositorio de GitHub aquí.
Encuéntrelo en PyPI aquí.
La historia
Comenzó con el caso del recuento. Nuestra empresa introdujo KPI relacionados con los ingresos por empleado, por lo que necesitábamos conocer la plantilla anual promedio de cada equipo. Ya había estado escribiendo scripts en Python, así que no me sentí intimidado.
Para obtener un recuento tuve que calcular el número de días hábiles que cada empleado pasó en la empresa durante el año. Los pandas se encargarían de ello en un segundo, pensé. Pero resultó que Pandas no podía.
El calendario empresarial ruso es complicado. Intercambian los días laborables con los sábados o domingos para llenar los huecos entre festivos y fines de semana. Por ejemplo, debe venir a trabajar un sábado de febrero para recibir un reembolso con un lunes gratuito anterior a un martes festivo en algún lugar de mayo.
El esquema para cada año es único. El calendario de días hábiles de Pandas solo admite modificaciones unidireccionales para observaciones de días festivos. Así, podría convertir un día laborable en un día libre, pero no al revés.
Luego estaban los operadores en el centro de llamadas y mi ansiedad giró en sentido contrario. Trabajan en turnos de diferente duración, un turno de entrada seguido de tres turnos de salida. Para obtener las estadísticas del call center no necesitaba el calendario de días hábiles. Sin embargo, tuve que contar el número de turnos de un operador en particular en un período de tiempo.
Y, por último, un problema poco convencional. En mi concesionario Honda local, los mecánicos trabajan en horarios semanales alternos: lunes, martes, sábado y domingo esta semana, y de miércoles a viernes la semana siguiente. Siempre quería ser atendido por un mecánico en particular, porque el otro una vez había estropeado los frenos. Quería una forma sencilla de determinar el próximo turno de "mi" mecánico.
Estos casos tienen un fundamento común. Sus soluciones se basarían en un cronograma de períodos de tiempo "de servicio" y "fuera de servicio". Deberíamos poder construir cronogramas estructurados de diversas formas, adecuados para diferentes casos de negocios. Las consultas y cálculos realizados sobre el cronograma deben distinguir entre períodos “de servicio” y “fuera de servicio”.
No pude encontrar un paquete de Python que proporcionara los medios para crear y consultar dichos programas. Dio la casualidad de que tuve algo de tiempo libre para escribirlo yo mismo.
El concepto
timeboard
es una biblioteca de Python que crea horarios de períodos de trabajo y realiza cálculos de calendario sobre ellos. Estos objetos en sí se denominan tableros de tiempo.
Hay tres pasos principales en el razonamiento sobre un tablero de tiempo.
Comienzas con un intervalo de tiempo que marca los límites de tu calendario. Todo quedará confinado a este intervalo. Se llama marco (de referencia). El marco consta de muebles bajos. Una unidad base es el período de tiempo más pequeño que necesita para medir su calendario. Por ejemplo, si razona en términos de días hábiles, entonces la unidad base es un día. Alternativamente, si crea un cronograma de turnos de varias horas, entonces la unidad base es de una hora.
En el siguiente paso, definirá las reglas para marcar el marco en turnos de trabajo. Los turnos de trabajo son períodos de tiempo que le interesan. Ellos conforman tu calendario. Son turnos de trabajo los que desea programar o contar. En un calendario de días hábiles estándar, el turno de trabajo es un día (y la unidad base también es un día, por lo que coinciden).
En un call center, un turno de trabajo es un período de varias horas en el que un turno particular de operadores está de servicio. Es probable que la unidad base sea de una hora, y cada turno de trabajo comprende un número (probablemente variable) de unidades base.
La secuencia de turnos de trabajo que llenan el marco se llama línea de tiempo.
Finalmente, crea uno o más horarios. Un cronograma es como una plantilla colocada sobre la línea de tiempo. Su propósito es diferenciar los turnos de trabajo de los de fuera de servicio.
Un cronograma necesita algo con lo que trabajar para poder declarar un turno de trabajo en servicio o fuera de servicio. Es por eso que proporciona una etiqueta para cada turno de trabajo, o más bien una regla para etiquetarlos mientras el marco está marcado en la línea de tiempo. Cada cronograma define una función selectora que inspecciona la etiqueta del turno de trabajo y devuelve Verdadero para los turnos de trabajo en servicio y Falso en caso contrario. A menos que la anule, una línea de tiempo va acompañada de la programación predeterminada cuyo selector devuelve el valor booleano de la etiqueta.
A veces se desea definir varios horarios para un mismo cronograma. Por ejemplo, en un call center, habrá un horario para todo el call center y un horario separado para cada equipo de operadores. El mismo turno de trabajo puede encontrarse de servicio en algunos horarios y fuera de servicio en otros.
Cronograma=cronograma + horarios. Más precisamente, un cronograma es una colección de horarios de trabajo basados en una línea de tiempo específica de turnos de trabajo construidos sobre una referencia <marco.
Una vez que tengas un cronograma, podrás realizar el trabajo útil: hacer cálculos de calendario para resolver problemas como los que se describen en el prólogo.
Cada cálculo realizado con timeboard tiene en cuenta las obligaciones. El método invocado "ve" sólo los turnos de trabajo con la tarea especificada e ignora los demás. Para revelar el deber de los turnos de trabajo, es necesario proporcionar un cronograma al método. Por lo tanto, cada cómputo en el tablero de tiempos está parametrizado con un turno y un horario.
De forma predeterminada, la tarea está "activada" y el horario es el horario predeterminado del tablero de tiempo. Por ejemplo, si llama a count()
sin argumentos en algún intervalo de un cronograma, obtendrá la cantidad de turnos de trabajo en el intervalo que se declaran en servicio según el cronograma predeterminado. Estos valores predeterminados facilitan la vida porque, en la práctica, querrás ocuparte principalmente de los turnos de trabajo de turno.
La API
La documentación completa del cronograma está disponible en Read the Docs.
El paquete se puede instalar con el pip install timeboard
habitual.
Configurar un cronograma
La forma más sencilla de comenzar es utilizar un calendario preconfigurado que se envía con el paquete. Tomemos un calendario de días hábiles habitual para los Estados Unidos.
>>> import timeboard.calendars.US as US >>> clnd = US.Weekly8x5()
El objeto clnd
es un tablero de tiempo (una instancia de la clase timeboard.Timeboard
). Tiene solo un horario predeterminado que selecciona los días laborables como turnos de trabajo mientras que los fines de semana, así como las observaciones de los feriados federales de EE. UU., se declaran fuera de servicio.
Las herramientas para crear su propio tablero de tiempo se revisarán brevemente más adelante, después de que veamos lo que puede hacer con un tablero de tiempo.
Juega con los turnos de trabajo
Llamar a una instancia de tablero de tiempo clnd()
con un único punto en el tiempo recupera el turno de trabajo que contiene este punto. Como tienes un turno de trabajo puedes consultar su función:
¿Una fecha determinada es un día hábil?
>>> ws = clnd('27 May 2017')>>> ws.is_on_duty()False
De hecho, era sábado.
También puedes mirar hacia el futuro o hacia el pasado desde el turno de trabajo actual:
¿Cuándo fue el siguiente día hábil?
>>> ws.rollforward()Workshift(6359) of 'D' at 2017-05-30
El turno de trabajo devuelto tiene el número de secuencia 6359 y representa el día 30 de mayo de 2017, que, por cierto, fue el martes después del feriado del Día de los Caídos.
Si termináramos el proyecto en 22 días hábiles a partir del 1 de mayo de 2017, ¿cuándo sería nuestra fecha límite?
>>> clnd('01 May 2017') + 22Workshift(6361) of 'D' at 2017-06-01
Esto es lo mismo que:
>>> clnd('01 May 2017').rollforward(22)Workshift(6361) of 'D' at 2017-06-01
Juega con intervalos
Llamar a clnd()
con un conjunto diferente de parámetros produce un objeto que representa un intervalo en el calendario. El siguiente intervalo contiene todos los turnos de trabajo del mes de mayo de 2017:
>>> may2017 = clnd('May 2017', period='M')
¿Cuántos días hábiles hubo en mayo?
>>> may2017.count()22
¿Cuántos días libres?
>>> may2017.count(duty='off')9
¿Cuántas horas de trabajo?
>>> may2017.worktime()176
Un empleado formó parte del personal desde el 3 de abril de 2017 hasta el 15 de mayo de 2017. ¿Qué parte del salario de abril les debía la empresa?
Tenga en cuenta que llamar a clnd()
con una tupla de dos puntos en el tiempo produce un intervalo que contiene todos los turnos de trabajo entre estos puntos, inclusive.
>>> time_in_company = clnd(('03 Apr 2017','15 May 2017'))>>> time_in_company.what_portion_of(clnd('Apr 2017', period='M'))1.0
De hecho, el 1 y 2 de abril de 2017 cayeron en fin de semana, por lo que, comenzando el día 3, el empleado revisó todos los días hábiles del mes.
¿Y qué porción del de mayo?
>>> time_in_company.what_portion_of(may2017)0.5
¿Cuántos días trabajó el empleado en mayo?
El operador de multiplicación devuelve la intersección de dos intervalos.
>>> (time_in_company * may2017).count()11
¿Cuántas horas?
>>> (time_in_company * may2017).worktime()88
Un empleado estuvo en plantilla desde el 1 de enero de 2016 hasta el 15 de julio de 2017. ¿Cuántos años llevaba esta persona trabajando para la empresa?
>>> clnd(('01 Jan 2016', '15 Jul 2017')).count_periods('A')1.5421686746987953
Construye tu propio cronograma
A modo de introducción, me limitaré a analizar dos ejemplos. Si parece demasiado empinado, busque una descripción detallada de las herramientas de construcción en la documentación del proyecto.
La declaración de importación para esta sección:
>>> import timeboard as tb
Permítanme volver a un horario de turnos de trabajo en el concesionario de automóviles que mencioné en el prólogo. Un mecánico trabaja los lunes, martes, sábado y domingo de esta semana, y los miércoles, jueves y viernes de la próxima semana; luego se repite el ciclo quincenal. El cronograma se crea mediante el siguiente código:
>>> biweekly = tb.Organizer(marker='W',... structure=[[1,1,0,0,0,1,1], [0,0,1,1,1,0,0]])>>> clnd = tb.Timeboard(base_unit_freq='D', ... start='01 Oct 2017', end='31 Dec 2018', ... layout=biweekly)
Tiene sentido examinar primero la última afirmación. Crea un tablero de tiempo llamado clnd
. Los primeros tres parámetros definen el marco como una secuencia de días ('D
') desde el 1 de octubre de 2017 hasta el 31 de diciembre de 2018. El parámetro layout
indica cómo organizar el marco. en el cronograma de los turnos de trabajo. Este trabajo se encarga a un Organizador
llamado biweekly
.
La primera declaración crea este Organizador
que toma dos parámetros: marker
y structure
. Usamos unmarcador
para colocar marcas en el marco. Las marcas son una especie de hitos que dividen el marco en subtramas o “tramos”. En el ejemplo, marker=’W’
coloca una marca al comienzo de cada semana calendario. Por tanto, cada lapso representa una semana.
El parámetro structure
indica cómo crear turnos de trabajo dentro de cada intervalo. El primer elemento de estructura
, la lista [1,1,0,0,0,1,1]
, se aplica al primer intervalo (es decir, a la primera semana). de nuestro calendario). Cada unidad base (es decir, cada día) dentro del lapso se convierte en un turno de trabajo. Los turnos de trabajo reciben etiquetas de la lista, en orden.
El segundo elemento de estructura
, la lista [0,0,1,1,1,0,0]
, se aplica de manera análoga al segundo lapso (la segunda semana) . Después de esto, como no tenemos más elementos, una estructura
se reproduce en ciclos. Por lo tanto, la tercera semana es atendida por el primer elemento de estructura
, la cuarta semana por el segundo, y así sucesivamente.
Como resultado, nuestra línea de tiempo se convierte en la secuencia de días etiquetados con el número 1
cuando el mecánico está de servicio y con el número 0
cuando no lo está. No hemos especificado ningún cronograma, porque el cronograma creado por defecto nos conviene. El programa predeterminado considera el valor booleano de la etiqueta, por lo que 1
se traduce en "en servicio" y cero en "fuera de servicio".
Con este cronograma podremos hacer cualquier tipo de cálculos que hayamos hecho anteriormente con el calendario laboral. Por ejemplo, si una persona estuvo empleada según este horario desde el 4 de noviembre de 2017 y el salario se paga mensualmente, ¿qué parte del salario de noviembre ha ganado el empleado?
>>> time_in_company = clnd(('4 Nov 2017', None))>>> nov2017 = clnd('Nov 2017', period='M')>>> time_in_company.what_portion_of(nov2017)0.8125
En el segundo ejemplo, construiremos un cronograma para un centro de llamadas. El call center funciona las 24 horas del día en turnos de diferente duración: de 08:00 a 18:00 (10 horas), de 18:00 a 02:00 (8 horas) y de 02:00 a 08:00 (6 horas). ). El horario de un operador consta de un turno de trabajo seguido de tres turnos fuera de servicio. Por tanto, se necesitan cuatro equipos de operadores. Se designan como "A", "B", "C" y "D".
>>> day_parts = tb.Marker(each='D', ... at=[{'hours':2}, {'hours':8}, {'hours':18}])>>> shifts = tb.Organizer(marker=day_parts, ... structure=['A', 'B', 'C', 'D'])>>> clnd = tb.Timeboard(base_unit_freq='H', ... start='01 Jan 2009 02:00', end='01 Jan 2019 01:59',... layout=shifts)>>> clnd.add_schedule(name='team_A', ... selector=lambda label: label=='A')
Hay cuatro diferencias clave con el caso del concesionario. Los examinaremos uno por uno.
Primero, la unidad base del marco ahora es un período de una hora (base_unit_freq='H'
) en lugar de un período de un día del calendario del concesionario.
En segundo lugar, el valor del parámetro marker
del Organizador ahora es un objeto complejo en lugar de una única frecuencia de calendario como era antes. Este objeto es una instancia de la clase Marker
. Se utiliza para definir reglas para colocar marcas en el marco cuando la simple división del marco en unidades de calendario uniformes no es suficiente. La firma del marcador de arriba es casi legible: dice: coloque una marca en cada día ('D') a las 02:00 horas, 08:00 horas y 18:00 horas.
En tercer lugar, el valor de la estructura
ahora es más simple: es una lista de un nivel de etiquetas de equipos. Cuando un elemento de la estructura
no es un iterable de etiquetas sino solo una etiqueta, su aplicación a un intervalo produce un único turno de trabajo que, literalmente, abarca el intervalo.
En nuestro ejemplo, el primer tramo comprende seis unidades básicas de una hora que comienzan a las 2, 3, 4... 7 de la mañana del 1 de enero de 2009. Todas estas unidades básicas se combinan en un turno de trabajo único con la etiqueta 'A'. . El segundo tramo comprende diez unidades básicas de una hora a partir de las 8, 9, 10... 17 horas. Estas unidades básicas se combinan en un turno de trabajo único con la etiqueta "B", y así sucesivamente. Cuando se han tomado todas las etiquetas, se reproduce la estructura, por lo que el quinto intervalo (08:00:00-17:59:59 del 1 de enero de 2009) se convierte en un turno de trabajo con la etiqueta "A".
En resumen, si un elemento de estructura
es una lista de etiquetas, cada unidad base del intervalo se convierte en un turno de trabajo y recibe una etiqueta de la lista. Si un elemento de estructura
tiene una sola etiqueta, todas las unidades base del tramo se combinan para formar un único turno de trabajo que recibe esta etiqueta.
Y finalmente, creamos explícitamente un cronograma para el equipo A. El cronograma predeterminado no cumple con nuestro propósito ya que vuelve "siempre en servicio". Esto es cierto para el centro de llamadas en su conjunto, pero no para un equipo en particular. Para el nuevo horario, proporcionamos el nombre y la función de selección que devuelve Verdadero para todos los turnos de trabajo etiquetados con "A". Para un uso práctico, también querrás crear los horarios para los otros equipos.
Es tan bueno trabajar con este cronograma como con cualquier otro. Sin embargo, en esta ocasión tendremos que especificar explícitamente el horario que queremos utilizar.
>>> schedule_A = clnd.schedules['team_A']
¿Cuántos turnos realizaron los operadores del equipo A en noviembre de 2017?
>>> nov2017 = clnd('Nov 2017', period='M', schedule=schedule_A)>>> nov2017.count()22
¿Y cuántas horas fueron en total?
>>> nov2017.worktime()176
Una persona estuvo empleada como operador en el equipo A desde el 4 de noviembre de 2017. El salario se paga mensualmente. ¿Qué parte del salario de noviembre ha ganado el empleado?
>>> time_in_company = clnd(('4 Nov 2017',None), schedule=schedule_A)>>> time_in_company.what_portion_of(nov2017)0.9090909090909091
Más casos de uso
Puede encontrar más casos de uso (tomado casi de la vida real) en el cuaderno jupyter que forma parte de la documentación del proyecto.
No dude en utilizar timeboard
y no dude en dejar comentarios o abrir problemas en GitHub.