Verifying a maintenance schedule#

In this example we are given the following scenario:

There are three identical assets X, Y and Z which require periodic maintenance. No more than one asset should be under maintenance at any time, in order to handle the workload. Futhermore any maintenance should occur within windows of opportunity which represent when maintenance will be least disruptive. Given a proposed schedule for 2021, verify these rules are respected, and analyse time usage.

We start by importing pandas and piso

In [1]: import pandas as pd

In [2]: import piso

Running the piso.register_accessors() function will add “piso” accessors to pandas.IntervalIndex and pandas.arrays.IntervalArray. Using accessors allows us to extend the functionality associated with these classes, without adding the methods directly.

In [3]: piso.register_accessors()

Next we load the data from a csv file and store it into a pandas.DataFrame. Each row of the dataframe corresponds to an interval of maintenance for a particular asset.

In [4]: data = pd.read_csv("./data/asset_maintenance.csv", parse_dates=["start", "end"], dayfirst=True)

In [5]: data
Out[5]: 
   asset               start                 end
0      X 2021-12-14 00:00:00 2021-12-17 07:00:00
1      X 2021-01-31 23:00:00 2021-02-01 00:00:00
2      X 2021-03-15 05:00:00 2021-03-21 17:00:00
3      X 2021-09-07 13:00:00 2021-09-13 22:00:00
4      X 2021-05-02 00:00:00 2021-05-07 10:00:00
5      X 2021-08-03 14:00:00 2021-08-05 00:00:00
6      Y 2021-11-14 00:00:00 2021-11-16 22:00:00
7      Y 2021-09-01 00:00:00 2021-09-02 13:00:00
8      Y 2021-06-23 05:00:00 2021-06-30 11:00:00
9      Y 2021-01-18 19:00:00 2021-01-27 08:00:00
10     Y 2021-05-28 00:00:00 2021-05-31 16:00:00
11     Y 2021-03-23 05:00:00 2021-03-25 00:00:00
12     Z 2021-07-06 21:00:00 2021-07-10 00:00:00
13     Z 2021-04-11 07:00:00 2021-04-18 05:00:00
14     Z 2021-10-03 00:00:00 2021-10-06 14:00:00
15     Z 2021-02-25 00:00:00 2021-02-26 22:00:00
16     Z 2021-11-21 09:00:00 2021-11-26 00:00:00
17     Z 2021-06-03 06:00:00 2021-06-04 00:00:00

To work with piso we need the data in interval arrays. The following code creates a pandas.Series, indexed by the assets X, Y and Z, where the values are instances of pandas.arrays.IntervalArray.

In [6]: maintenance = (
   ...:     data
   ...:     .groupby("asset")
   ...:     .apply(
   ...:         lambda df: pd.arrays.IntervalArray.from_arrays(
   ...:             df["start"],
   ...:             df["end"],
   ...:             closed="left",
   ...:         ),
   ...:     )
   ...: )
   ...: 

In [7]: maintenance
Out[7]: 
asset
X    [[2021-12-14, 2021-12-17 07:00:00), [2021-01-3...
Y    [[2021-11-14, 2021-11-16 22:00:00), [2021-09-0...
Z    [[2021-07-06 21:00:00, 2021-07-10), [2021-04-1...
dtype: object

Checking that no more than one asset is under maintenance at any time is equivalent to checking that the sets corresponding to each interval array are disjoint. This is as simple as the following code, where we unpack the values of the maintenance Series as arguments to piso.isdisjoint().

In [8]: piso.isdisjoint(*maintenance.values)
Out[8]: True

The windows in which maintenance is preferred is described by the following data

In [9]: window_df = pd.read_csv(
   ...:     "./data/maintenance_windows.csv",
   ...:     parse_dates=["start", "end"],
   ...:     dayfirst=True,
   ...: )
   ...: 

In [10]: window_df
Out[10]: 
        start        end
0  2021-01-18 2021-02-01
1  2021-02-25 2021-03-05
2  2021-03-15 2021-03-25
3  2021-04-10 2021-04-20
4  2021-05-02 2021-05-09
5  2021-05-28 2021-06-04
6  2021-06-20 2021-07-10
7  2021-08-01 2021-08-05
8  2021-09-01 2021-09-14
9  2021-10-03 2021-10-08
10 2021-11-14 2021-11-26
11 2021-12-14 2021-12-22

As before, we transform this to an interval array

In [11]: windows = pd.arrays.IntervalArray.from_arrays(
   ....:     window_df["start"],
   ....:     window_df["end"],
   ....:     closed="left",
   ....: )
   ....: 

In [12]: windows
Out[12]: 
<IntervalArray>
[[2021-01-18, 2021-02-01), [2021-02-25, 2021-03-05), [2021-03-15, 2021-03-25), [2021-04-10, 2021-04-20), [2021-05-02, 2021-05-09) ... [2021-08-01, 2021-08-05), [2021-09-01, 2021-09-14), [2021-10-03, 2021-10-08), [2021-11-14, 2021-11-26), [2021-12-14, 2021-12-22)]
Length: 12, dtype: interval[datetime64[ns], left]

Checking that the maintenance occurs within the preferred windows can be done by checking that the set corresponding to the windows interval array is a superset of each of the sets corresponding to the asset interval arrays. Instead of doing this for each asset we can check against the union of these sets.

In [13]: combined_maintenance = piso.union(*maintenance.values)

In [14]: windows.piso.issuperset(combined_maintenance, squeeze=True)
Out[14]: True

Now let’s answer some questions using piso, specifically piso.coverage() and its accessor counterpart.

What fraction of the year 2021 constitutes maintenance window opportunities?

In [15]: windows.piso.coverage(pd.Interval(pd.Timestamp("2021"), pd.Timestamp("2022")))
Out[15]: 0.3232876712328767

How many days in each month in 2021 constitute maintenance window opportunities?

For this we’ll create a pandas.IntervalIndex for the months, then construct a pandas.Series with a monthly pandas.PeriodIndex.

In [16]: months = pd.IntervalIndex.from_breaks(pd.date_range("2021", "2022", freq="MS"))

In [17]: pd.Series(
   ....:     [windows.piso.coverage(month)*month.length for month in months],
   ....:     index = months.left.to_period()
   ....: )
   ....: 
Out[17]: 
2021-01   14 days
2021-02    4 days
2021-03   14 days
2021-04   10 days
2021-05   11 days
2021-06   14 days
2021-07    9 days
2021-08    4 days
2021-09   13 days
2021-10    5 days
2021-11   12 days
2021-12    8 days
Freq: M, dtype: timedelta64[ns]

What fraction of the time in window opportunities is utilised by the combined maintenance?

In [18]: combined_maintenance.piso.coverage(windows)
Out[18]: 0.5903954802259888

What fraction of the combined maintenance is occupied by each asset

In [19]: maintenance.apply(piso.coverage, domain=combined_maintenance)
Out[19]: 
asset
X    0.330742
Y    0.369019
Z    0.300239
dtype: float64