Simplifiez votre planification complexe avec timeboard, une bibliothèque Python

timeboardest une bibliothèque Python qui crée des horaires de périodes de travail et effectue des calculs de calendrier sur celles-ci. Vous pouvez créer des calendriers de jour ouvrable standard ainsi qu'une variété d'autres horaires, simples ou complexes.

Vous pouvez trouver la documentation ici.

Découvrez le dépôt GitHub ici.

Trouvez-le sur PyPI ici.

L'histoire

Cela a commencé avec le cas des effectifs. Notre société a introduit des KPI impliquant le chiffre d'affaires par employé, nous devions donc connaître l'effectif annuel moyen de chaque équipe. J'avais déjà écrit des scripts Python, donc je n'étais pas intimidé.

Pour obtenir un effectif, j'ai dû calculer le nombre de jours ouvrables que chaque employé a passé dans l'entreprise au cours de l'année. Les pandas le géreraient en une seconde, pensai-je. Mais il est apparu que les Pandas ne pouvaient pas.

Le calendrier des affaires russe est lourd. Ils troquent les jours de la semaine avec les samedis ou dimanches pour combler les lacunes entre les vacances et les week-ends. Par exemple, vous devez venir travailler un samedi de février pour être remboursé avec un lundi gratuit précédant un mardi férié quelque part en mai.

Le schéma de chaque année est unique. Le calendrier des jours ouvrables de Pandas ne prend en charge que les modifications à sens unique pour les observations de vacances. Ainsi, je pourrais transformer un jour ouvrable en jour de congé, mais pas l'inverse.

Ensuite, il y avait des opérateurs dans le centre d'appels, et mon anxiété a basculé dans l'autre sens. Ils travaillent par quarts de durée variable, et un quart de travail suivi de trois quarts de travail. Pour obtenir les statistiques du centre d'appels, je n'avais pas besoin du calendrier des jours ouvrables. Pourtant, je devais compter le nombre de quarts de travail d'un opérateur particulier dans une période de temps.

Et enfin, un problème décalé. Dans mon concessionnaire Honda local, les mécaniciens travaillent selon des horaires hebdomadaires alternés: lundi, mardi, samedi et dimanche cette semaine, et du mercredi au vendredi la semaine prochaine. Je voulais toujours être servi par un mécanicien en particulier, car l'autre avait une fois gâché les freins. Je voulais un moyen simple de déterminer le prochain quart de travail de «mon» mécanicien.

Ces cas ont un fondement commun. Leurs solutions reposent sur un calendrier des périodes «en service» et «hors service». Nous devrions être en mesure de construire des calendriers structurés de manière variée et adaptés à différentes analyses de rentabilisation. Les requêtes et les calculs exécutés sur le calendrier doivent faire la distinction entre les périodes «de service» et «hors service».

Je n'ai pas trouvé de package Python qui fournisse les moyens de créer et d'interroger ces horaires. En fait, j'ai eu du temps libre pour l'écrire moi-même.

Le concept

timeboardest une bibliothèque Python qui crée des horaires de périodes de travail et effectue des calculs de calendrier sur celles-ci. Ces objets eux-mêmes sont appelés timeboards.

Il y a trois étapes majeures dans le raisonnement sur un calendrier.

Vous commencez avec un intervalle de temps qui définit les limites de votre calendrier. Tout sera confiné à cet intervalle. C'est ce qu'on appelle le cadre (de référence). Le cadre se compose d'unités de base. Une unité de base est la plus petite période de temps dont vous avez besoin pour évaluer votre calendrier. Par exemple, si vous raisonnez en jours ouvrables, l'unité de base est un jour. Sinon, si vous créez un programme de postes de plusieurs heures, l'unité de base est d'une heure.

À l'étape suivante, vous définissez les règles de marquage du cadre en équipes de travail. Les horaires de travail sont des périodes qui vous tiennent à cœur. Ils composent votre calendrier. Ce sont des heures de travail que vous souhaitez planifier ou compter. Dans un calendrier de jours ouvrables standard, le quart de travail est un jour (et l'unité de base est également un jour, donc ils coïncident).

Dans un centre d'appels, le quart de travail est une période de plusieurs heures pendant laquelle un quart particulier d'opérateurs est en service. L'unité de base durera probablement une heure et chaque période de travail comprend un nombre (probablement variable) d'unités de base.

La séquence de périodes de travail remplissant le cadre est appelée la chronologie.

Enfin, vous créez un ou plusieurs horaires. Un calendrier est comme un pochoir posé sur la chronologie. Son but est de distinguer les heures de travail en service des heures de repos.

Un horaire a besoin de quelque chose avec lequel travailler pour déclarer un quart de travail en service ou en congé. C'est pourquoi vous fournissez une étiquette pour chaque période de travail, ou plutôt une règle pour les étiqueter pendant que l'image est balisée dans la chronologie. Chaque horaire définit une fonction de sélection qui inspecte l'étiquette du quart de travail et renvoie True pour les heures de travail en service et False sinon. Sauf si vous le remplacez, une chronologie est accompagnée de la planification par défaut dont le sélecteur renvoie la valeur booléenne de l'étiquette.

Parfois, vous souhaitez définir plusieurs horaires pour la même chronologie. Par exemple, dans un centre d'appels, il y aura l'horaire pour le centre d'appels dans son ensemble et un horaire distinct pour chaque équipe d'opérateurs. Le même quart de travail peut être trouvé en service sous certains horaires et hors service sous les autres.

Timeboard = chronologie + horaires. Plus précisément, le calendrier est un ensemble d' horaires de travail basés sur une chronologie spécifique de quarts de travail construite sur un cadre de référence .

Une fois que vous avez un calendrier, vous pouvez effectuer le travail utile: faire des calculs de calendrier afin de résoudre les problèmes tels que ceux décrits dans le prologue.

Chaque calcul effectué avec le timeboard est sensible au devoir. La méthode invoquée ne «voit» que les heures de travail avec le devoir spécifié et ignore les autres. Afin de révéler le devoir des équipes de travail, la méthode doit être programmée. Par conséquent, chaque calcul sur le tableau des temps est paramétré avec un devoir et un horaire.

Par défaut, le devoir est «activé» et l'horaire est l'horaire par défaut du calendrier. Par exemple, si vous appelez count()sans argument sur un certain intervalle d'un calendrier, vous obtiendrez le nombre de postes de travail dans l'intervalle qui sont déclarés en service selon le calendrier par défaut. Ces défauts vous facilitent la vie car dans la pratique, vous voudrez surtout vous occuper de quarts de travail en service.

L'API

La documentation complète du calendrier est disponible sur Read the Docs.

Le package peut être installé avec l'habituel pip install timeboard.

Configurer un calendrier

La façon la plus simple de commencer consiste à utiliser un calendrier préconfiguré qui est livré avec le package. Prenons un calendrier de jour ouvrable régulier pour les États-Unis.

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