Simplifique su compleja programación con Timeboard, una biblioteca de Python

timeboardes 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.

Puede encontrar la documentación aquí.

Consulte 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 de 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 ha pasado en la empresa durante el año. Los pandas lo manejarían en un segundo, pensé. Pero resultó que Pandas no podía.

El calendario comercial ruso es engorroso. Intercambian los días de la semana con los sábados o domingos para llenar los vacíos entre festivos y fines de semana. Por ejemplo, tienes que venir a trabajar un sábado de febrero para que te reembolsen un lunes gratis antes de un martes festivo en algún lugar de mayo.

El esquema de cada año es único. El calendario de días hábiles de Pandas solo admite modificaciones unidireccionales para las observaciones de días festivos. Por lo tanto, podría convertir un día laboral en un día libre, pero no al revés.

Luego hubo operadores en el centro de llamadas, y mi ansiedad cambió en sentido contrario. Trabajan en turnos de diversa duración, y un turno dentro seguido de tres turnos fuera. Para obtener las estadísticas del centro de llamadas, no necesitaba el calendario de días hábiles. Sin embargo, tuve que contar el número de turnos de operador en particular en un período de tiempo.

Y finalmente, un problema poco convencional. En mi concesionario Honda local, los mecánicos trabajan en horarios semanales alternativos: lunes, martes, sábado y domingo esta semana, y de miércoles a viernes la próxima semana. Quería que siempre me atendiera 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 una base común. Sus soluciones se basarían en un calendario de períodos de tiempo "en servicio" y "fuera de servicio". Deberíamos ser capaces de construir programas estructurados de diversas maneras, adecuados para diferentes casos comerciales. Las consultas y los cálculos que se ejecutan a lo largo del programa deben distinguir entre los períodos "en servicio" y "fuera de servicio".

No pude encontrar un paquete de Python que proporcionara los medios para construir y consultar dichos horarios. Resultó que tuve algo de tiempo libre para escribirlo yo mismo.

El concepto

timeboardes una biblioteca de Python que crea horarios de períodos de trabajo y realiza cálculos de calendario sobre ellos. Estos mismos objetos se denominan tablas de tiempo.

Hay tres pasos principales en el razonamiento sobre un calendario.

Comienzas con un intervalo de tiempo que establece los límites de tu calendario. Todo se limitará a este intervalo. Se llama marco (de referencia). El marco consta de unidades base. 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 programa de turnos de varias horas, entonces la unidad base es una hora.

En el siguiente paso, define las reglas para marcar el marco en turnos de trabajo. Los turnos de trabajo son períodos de tiempo que le interesan. Ellos componen tu calendario. Son los 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 centro de llamadas, el turno laboral 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 denomina línea de tiempo.

Finalmente, crea uno o más horarios. Un horario es como una plantilla colocada sobre la línea de tiempo. Su propósito es diferenciar los turnos de trabajo en servicio de los fuera de servicio.

Un horario necesita algo con lo que trabajar para 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 horario define una función de selección que inspecciona la etiqueta del turno de trabajo y devuelve Verdadero para los turnos de trabajo y Falso en caso contrario. A menos que lo 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, desea definir varios horarios para la misma línea de tiempo. Por ejemplo, en un centro de llamadas, habrá un horario para el centro de llamadas en su conjunto y un horario separado para cada equipo de operadores. El mismo turno de trabajo puede encontrarse en servicio en algunos horarios y fuera de servicio en otros.

Horario = cronograma + horarios. Más precisamente, el horario es una colección de horarios de trabajo basados ​​en una línea de tiempo específica de turnos de trabajo construida sobre un marco de referencia .

Una vez que tenga un tablero de tiempo, puede realizar el trabajo útil: hacer cálculos de calendario para resolver los problemas como los descritos en el prólogo.

Cada cálculo realizado con el cronómetro es consciente del deber. El método invocado "ve" solo los turnos de trabajo con el deber especificado e ignora los demás. Para revelar el deber de los turnos de trabajo, el método debe tener un horario. Por lo tanto, cada cálculo en el tablero de tiempo está parametrizado con un deber y un horario.

De forma predeterminada, el deber está "activado" y el horario es el horario predeterminado del horario. Por ejemplo, si llama count()sin argumentos en algún intervalo de un horario, obtendrá el número de turnos de trabajo en el intervalo que se declaran en servicio según el horario predeterminado. Estos valores predeterminados facilitan la vida porque, en la práctica, querrá ocuparse principalmente de los turnos de trabajo.

La API

La documentación completa del horario está disponible en Read the Docs.

El paquete se puede instalar con el habitual pip install timeboard.

Configurar un horario

La forma más sencilla de empezar es utilizar un calendario preconfigurado que se envía con el paquete. Tomemos un calendario de días hábiles normales para los Estados Unidos.

 >>> import timeboard.calendars.US as US >>> clnd = US.Weekly8x5()

clnd object is a timeboard (an instance of timeboard.Timeboard class). It has only one default schedule which selects weekdays as on-duty workshifts while weekends, as well as observations of US federal holidays, are declared off duty.

The tools for building your own timeboard will be briefly reviewed later on after we look at what you can do with a timeboard.

Play with workshifts

Calling a timeboard instance clnd() with a single point in time retrieves the workshift that contains this point. How that you have a workshift you can query its duty:

Is a certain date a business day?

>>> ws = clnd('27 May 2017')>>> ws.is_on_duty()False

Indeed, it was a Saturday.

You can also look into the future or in the past from the current workshift:

When was the next business day?

>>> ws.rollforward()Workshift(6359) of 'D' at 2017–05–30

The returned workshift has the sequence number of 6359 and represents the day of 30 May 2017, which, by the way, was the Tuesday after the Memorial Day holiday.

If we were to finish the project in 22 business days starting on 01 May 2017, when would be our deadline?

>>> clnd('01 May 2017') + 22Workshift(6361) of 'D' at 2017–06–01

This is the same as:

>>> clnd('01 May 2017').rollforward(22)Workshift(6361) of 'D' at 2017–06–01

Play with intervals

Calling clnd() with a different set of parameters produces an object representing an interval on the calendar. The interval below contains all workshifts of the month of May 2017:

>>> may2017 = clnd('May 2017', period="M")

How many business days were there in May?

>>> may2017.count()22

How many days off?

>>> may2017.count(duty='off')9

How many working hours?

>>> may2017.worktime()176

An employee was on the staff from April 3, 2017, to May 15, 2017. What portion of April’s salary did the company owe them?

Note that calling clnd() with a tuple of two points in time produces an interval containing all workshifts between these points, inclusively.

>>> time_in_company = clnd(('03 Apr 2017','15 May 2017'))>>> time_in_company.what_portion_of(clnd('Apr 2017', period="M"))1.0

Indeed, the 1st and the 2nd of April in 2017 fell on the weekend, therefore, having started on the 3rd, the employee checked out all the working days in the month.

And what portion of May’s?

>>> time_in_company.what_portion_of(may2017)0.5

How many days had the employee worked in May?

The multiplication operator returns the intersection of two intervals.

>>> (time_in_company * may2017).count()11

How many hours?

>>> (time_in_company * may2017).worktime()88

An employee was on the staff from 01 Jan 2016 to 15 Jul 2017. How many years had this person worked for the company?

>>> clnd(('01 Jan 2016', '15 Jul 2017')).count_periods('A')1.5421686746987953

Build your own timeboard

For the purpose of introduction, I will just plunge into two examples. If it seems too steep, please, find the thorough discussion of the construction tools in the project documentation.

The import statement for this section:

>>> import timeboard as tb

Let me return to a schedule of workshifts in the car dealership which I mentioned in the prologue. A mechanic works on Monday, Tuesday, Saturday, and Sunday this week, and on Wednesday, Thursday, and Friday next week; then the bi-weekly cycle repeats. The timeboard is created by the following code:

>>> 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)

It makes sense to look into the last statement first. It creates a timeboard named clnd. The first three parameters define the frame to be a sequence of days (‘D’) from 01 Oct 2017 to 31 Dec 2018. The layout parameter tells how to organize the frame into the timeline of workshifts. This job is commissioned to an Organizer named biweekly.

The first statement creates this Organizer which takes two parameters: marker and structure. We use amarker to place marks on the frame. The marks are kind of milestones which divide the frame into subframes, or “spans”. In the example marker=’W’ puts a mark at the beginning of each calendar week. Therefore, each span represents a week.

The structure parameter tells how to create workshifts within each span. The first element of structure, the list [1,1,0,0,0,1,1], is applied to the first span (i.e. to the first week of our calendar). Each base unit (that is, each day) within the span becomes a workshift. The workshifts receive labels from the list, in order.

The second element of structure, the list [0,0,1,1,1,0,0], is analogously applied to the second span (the second week). After this, since we’ve gotten no more elements, a structure is replayed in cycles. Hence, the third week is serviced by the first element of structure, the fourth week by the second, and so on.

As a result, our timeline becomes the sequence of days labeled with the number 1 when the mechanic is on duty and with the number 0 when he or she is not. We have not specified any schedule, because the schedule which is built by default suits us fine. The default schedule considers the boolean value of the label, so 1 translates into ‘on duty’, and zero into ‘off duty’.

With this timeboard, we can do any type of calculations that we have done earlier with the business calendar. For example, if a person was employed to this schedule from November 4, 2017, and salary is paid monthly, what portion of November’s salary has the employee earned?

>>> time_in_company = clnd(('4 Nov 2017', None))>>> nov2017 = clnd('Nov 2017', period="M")>>> time_in_company.what_portion_of(nov2017)0.8125

In the second example we will build a timeboard for a call center. The call center operates round-the-clock in shifts of varying length: 08:00 to 18:00 (10 hours), 18:00 to 02:00 (8 hours), and 02:00 to 08:00 (6 hours). An operator’s schedule consists of one on-duty shift followed by three off-duty shifts. Hence, four teams of operators are needed. They are designated as ‘A’, ‘B’, ‘C’, and ‘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')

There are four key differences from the dealership case. We will examine them one by one.

First, the frame’s base unit is now a one-hour period (base_unit_freq='H') instead of a one-day period of the dealership’s calendar.

Second, the value of the marker parameter of the Organizer is now a complex object instead of a single calendar frequency it was before. This object is an instance of Marker class. It is used to define rules for placing marks on the frame when the simple division of the frame into uniform calendar units is not sufficient. The signature of the Marker above is almost readable — it says: place a mark on each day (‘D’) at 02:00 hours, 08:00 hours, and 18:00 hours.

Third, the value of the structure is now simpler: it is a one-level list of teams’ labels. When an element of the structure is not an iterable of labels but just one label, its application to a span produces a single workshift which, literally, spans the span.

In our example, the very first span comprises six one-hour base units starting at 2, 3, 4 … 7 o’clock in the morning of 01 Jan 2009. All these base units are combined into the single workshift with label ‘A’. The second span comprises ten one-hour base units starting at 8, 9, 10 … 17 o’clock. These base units are combined into the single workshift with label ‘B’, and so on. When all labels have been taken, the structure is replayed, so the fifth span (08:00:00–17:59:59 on 01 Jan 2009) becomes a workshift with label ‘A’.

To recap, if an element of structure is a list of labels, each base unit of the span becomes a workshift and receives a label from the list. If an element of structure is a single label, all base units of the span are combined to form a single workshift which receives this label.

And finally, we explicitly created a schedule for team A. The default schedule does not serve our purpose as it returns “always on duty”. This is true for the call center as a whole but not so for a particular team. For the new schedule, we supply the name and the selector function which returns True for all workshifts labeled with ‘A’. For the practical use, you will want to create the schedules for the other teams as well.

This timeboard is as good to work with as any other. However, this time we will have to explicitly specify the schedule we want to use.

>>> schedule_A = clnd.schedules['team_A']

How many shifts did the operators of team A sit in November 2017?

>>> nov2017 = clnd('Nov 2017', period="M", schedule=schedule_A)>>> nov2017.count()22

And how many hours were there in total?

>>> nov2017.worktime()176

A person was employed as an operator in team A from November 4, 2017. Salary is paid monthly. What portion of November’s salary has the employee earned?

>>> time_in_company = clnd(('4 Nov 2017',None), schedule=schedule_A)>>> time_in_company.what_portion_of(nov2017)0.9090909090909091

More use cases

You can find more use cases (taken almost from real life) in the jupyter notebook which is the part of the project documentation.

Please feel free to use timeboard and do not hesitate to leave feedback or open issues on GitHub .