Problem Description
A major problem faced by businesses in asset-heavy industries such as manufacturing is the significant costs that are associated with delays in the production process due to mechanical problems. Most of these businesses are interested in predicting these problems in advance so that they can proactively prevent the problems before they occur which will reduce the costly impact caused by downtime. Please refer to the playbook for predictive maintenance for a detailed explanation of common use cases in predictive maintenance and modelling approaches.
In this notebook, we follow the ideas from the playbook referenced above and aim to provide the steps of implementing a predictive model for a scenario which is based on a synthesis of multiple real-world business problems. This example brings together common data elements observed among many predictive maintenance use cases and the data itself is created by data simulation methods.
The business problem for this example is about predicting problems caused by component failures such that the question “What is the probability that a machine will fail in the near future due to a failure of a certain component?” can be answered. The problem is formatted as a multi-class classification problem and a machine learning algorithm is used to create the predictive model that learns from historical data collected from machines. In the following sections, we go through the steps of implementing such a model which are feature engineering, label construction, training and evaluation. First, we start by explaining the data sources in the next section.
Data Sources
Common data sources for predictive maintenance problems are :
- Failure history: The failure history of a machine or component within the machine.
- Maintenance history: The repair history of a machine, e.g. error codes, previous maintenance activities or component replacements.
- Machine conditions and usage: The operating conditions of a machine e.g. data collected from sensors.
- Machine features: The features of a machine, e.g. engine size, make and model, location.
- Operator features: The features of the operator, e.g. gender, past experience
The data for this example comes from 4 different sources which are real-time telemetry data collected from machines, error messages, historical maintenance records that include failures and machine information such as type and age.
import pandas as pd
telemetry = pd.read_csv('PdM_telemetry.csv')
errors = pd.read_csv('PdM_errors.csv')
maint = pd.read_csv('PdM_maint.csv')
failures = pd.read_csv('PdM_failures.csv')
machines = pd.read_csv('PdM_machines.csv')
# format datetime field which comes in as string
telemetry['datetime'] = pd.to_datetime(telemetry['datetime'], format="%Y-%m-%d %H:%M:%S")
print("Total number of telemetry records: %d" % len(telemetry.index))
print(telemetry.head())
telemetry.describe()
Total number of telemetry records: 876100
datetime machineID volt rotate pressure \
0 2015-01-01 06:00:00 1 176.217853 418.504078 113.077935
1 2015-01-01 07:00:00 1 162.879223 402.747490 95.460525
2 2015-01-01 08:00:00 1 170.989902 527.349825 75.237905
3 2015-01-01 09:00:00 1 162.462833 346.149335 109.248561
4 2015-01-01 10:00:00 1 157.610021 435.376873 111.886648
vibration
0 45.087686
1 43.413973
2 34.178847
3 41.122144
4 25.990511
|
machineID |
volt |
rotate |
pressure |
vibration |
| count |
876100.000000 |
876100.000000 |
876100.000000 |
876100.000000 |
876100.000000 |
| mean |
50.500000 |
170.777736 |
446.605119 |
100.858668 |
40.385007 |
| std |
28.866087 |
15.509114 |
52.673886 |
11.048679 |
5.370361 |
| min |
1.000000 |
97.333604 |
138.432075 |
51.237106 |
14.877054 |
| 25% |
25.750000 |
160.304927 |
412.305714 |
93.498181 |
36.777299 |
| 50% |
50.500000 |
170.607338 |
447.558150 |
100.425559 |
40.237247 |
| 75% |
75.250000 |
181.004493 |
482.176600 |
107.555231 |
43.784938 |
| max |
100.000000 |
255.124717 |
695.020984 |
185.951998 |
76.791072 |
Telemetry
The first data source is the telemetry time-series data which consists of voltage, rotation, pressure, and vibration measurements collected from 100 machines in real time averaged over every hour collected during the year 2015. Below, we display the first 10 records in the dataset. A summary of the whole dataset is also provided.
%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns
plot_df = telemetry.loc[(telemetry['machineID'] == 1) &
(telemetry['datetime'] > pd.to_datetime('2015-01-01')) &
(telemetry['datetime'] <pd.to_datetime('2015-02-01')),
['datetime','volt']]
sns.set_style("darkgrid")
plt.figure(figsize=(20, 8))
plt.plot(plot_df['datetime'], plot_df['volt'])
plt.ylabel('voltage')
# make x-axis ticks legible
adf = plt.gca().get_xaxis().get_major_formatter()
adf.scaled[1.0] = '%m-%d-%Y'
plt.xlabel('Date')
<matplotlib.text.Text at 0x2564dac0780>

Errors
The second major data source is the error logs. These are non-breaking errors thrown while the machine is still operational and do not constitute as failures. The error date and times are rounded to the closest hour since the telemetry data is collected at an hourly rate.
# format of datetime field which comes in as string
errors['datetime'] = pd.to_datetime(errors['datetime'],format = '%Y-%m-%d %H:%M:%S')
errors['errorID'] = errors['errorID'].astype('category')
print("Total Number of error records: %d" %len(errors.index))
errors.head()
Total Number of error records: 3919
|
datetime |
machineID |
errorID |
| 0 |
2015-01-03 07:00:00 |
1 |
error1 |
| 1 |
2015-01-03 20:00:00 |
1 |
error3 |
| 2 |
2015-01-04 06:00:00 |
1 |
error5 |
| 3 |
2015-01-10 15:00:00 |
1 |
error4 |
| 4 |
2015-01-22 10:00:00 |
1 |
error4 |
sns.set_style("darkgrid")
plt.figure(figsize=(20, 8))
errors['errorID'].value_counts().plot(kind='bar')
plt.ylabel('Count')
errors['errorID'].value_counts()
error1 1010
error2 988
error3 838
error4 727
error5 356
Name: errorID, dtype: int64

Maintenance
These are the scheduled and unscheduled maintenance records which correspond to both regular inspection of components as well as failures. A record is generated if a component is replaced during the scheduled inspection or replaced due to a breakdown. The records that are created due to breakdowns will be called failures which is explained in the later sections. Maintenance data has both 2014 and 2015 records.
maint['datetime'] = pd.to_datetime(maint['datetime'], format='%Y-%m-%d %H:%M:%S')
maint['comp'] = maint['comp'].astype('category')
print("Total Number of maintenance Records: %d" %len(maint.index))
maint.head()
Total Number of maintenance Records: 3286
|
datetime |
machineID |
comp |
| 0 |
2014-06-01 06:00:00 |
1 |
comp2 |
| 1 |
2014-07-16 06:00:00 |
1 |
comp4 |
| 2 |
2014-07-31 06:00:00 |
1 |
comp3 |
| 3 |
2014-12-13 06:00:00 |
1 |
comp1 |
| 4 |
2015-01-05 06:00:00 |
1 |
comp4 |
sns.set_style("darkgrid")
plt.figure(figsize=(10, 4))
maint['comp'].value_counts().plot(kind='bar')
plt.ylabel('Count')
maint['comp'].value_counts()
comp2 863
comp4 811
comp3 808
comp1 804
Name: comp, dtype: int64

Machines
This data set includes some information about the machines: model type and age (years in service).
machines['model'] = machines['model'].astype('category')
print("Total number of machines: %d" % len(machines.index))
machines.head()
Total number of machines: 100
|
machineID |
model |
age |
| 0 |
1 |
model3 |
18 |
| 1 |
2 |
model4 |
7 |
| 2 |
3 |
model3 |
8 |
| 3 |
4 |
model3 |
7 |
| 4 |
5 |
model3 |
2 |
sns.set_style("darkgrid")
plt.figure(figsize=(15, 6))
_, bins, _ = plt.hist([machines.loc[machines['model'] == 'model1', 'age'],
machines.loc[machines['model'] == 'model2', 'age'],
machines.loc[machines['model'] == 'model3', 'age'],
machines.loc[machines['model'] == 'model4', 'age']],
20, stacked=True, label=['model1', 'model2', 'model3', 'model4'])
plt.xlabel('Age (yrs)')
plt.ylabel('Count')
plt.legend()
<matplotlib.legend.Legend at 0x2564dda9898>

Failures
These are the records of component replacements due to failures. Each record has a date and time, machine ID, and failed component type.
# format datetime field which comes in as string
failures['datetime'] = pd.to_datetime(failures['datetime'], format="%Y-%m-%d %H:%M:%S")
failures['failure'] = failures['failure'].astype('category')
print("Total number of failures: %d" % len(failures.index))
failures.head()
Total number of failures: 761
|
datetime |
machineID |
failure |
| 0 |
2015-01-05 06:00:00 |
1 |
comp4 |
| 1 |
2015-03-06 06:00:00 |
1 |
comp1 |
| 2 |
2015-04-20 06:00:00 |
1 |
comp2 |
| 3 |
2015-06-19 06:00:00 |
1 |
comp4 |
| 4 |
2015-09-02 06:00:00 |
1 |
comp4 |
sns.set_style("darkgrid")
plt.figure(figsize=(15, 4))
failures['failure'].value_counts().plot(kind='bar')
plt.ylabel('Count')
failures['failure'].value_counts()
comp2 259
comp1 192
comp4 179
comp3 131
Name: failure, dtype: int64

Feature Engineering
The first step in predictive maintenance applications is feature engineering which requires bringing the different data sources together to create features that best describe a machines’s health condition at a given point in time. In the next sections, several feature engineering methods are used to create features based on the properties of each data source.
Lag Features from Telemetry
Telemetry data almost always comes with time-stamps which makes it suitable for calculating lagging features. A common method is to pick a window size for the lag features to be created and compute rolling aggregate measures such as mean, standard deviation, minimum, maximum, etc. to represent the short term history of the telemetry over the lag window. In the following, rolling mean and standard deviation of the telemetry data over the last 3 hour lag window is calculated for every 3 hours.
# Calculate mean values for telemetry features
temp = []
fields = ['volt', 'rotate', 'pressure', 'vibration']
for col in fields:
temp.append(pd.pivot_table(telemetry,
index='datetime',
columns='machineID',
values=col).resample('3H', closed='left', label='right', how='mean').unstack())
telemetry_mean_3h = pd.concat(temp, axis=1)
telemetry_mean_3h.columns = [i + 'mean_3h' for i in fields]
telemetry_mean_3h.reset_index(inplace=True)
C:\Program Files\Microsoft\ML Server\PYTHON_SERVER\lib\site-packages\ipykernel_launcher.py:8: FutureWarning: how in .resample() is deprecated
the new syntax is .resample(...).mean()
# Calculate mean values for telemetry features
temp = []
fields = ['volt', 'rotate', 'pressure', 'vibration']
for col in fields:
temp.append(pd.pivot_table(telemetry,
index='datetime',
columns='machineID',
values=col).resample('3H', closed='left', label='right', how='mean').unstack())
telemetry_mean_3h = pd.concat(temp, axis=1)
telemetry_mean_3h.columns = [i + 'mean_3h' for i in fields]
telemetry_mean_3h.reset_index(inplace=True)
# repeat for standard deviation
temp = []
for col in fields:
temp.append(pd.pivot_table(telemetry,
index='datetime',
columns='machineID',
values=col).resample('3H', closed='left', label='right', how='std').unstack())
telemetry_sd_3h = pd.concat(temp, axis=1)
telemetry_sd_3h.columns = [i + 'sd_3h' for i in fields]
telemetry_sd_3h.reset_index(inplace=True)
telemetry_mean_3h.head()
C:\Program Files\Microsoft\ML Server\PYTHON_SERVER\lib\site-packages\ipykernel_launcher.py:8: FutureWarning: how in .resample() is deprecated
the new syntax is .resample(...).mean()
C:\Program Files\Microsoft\ML Server\PYTHON_SERVER\lib\site-packages\ipykernel_launcher.py:19: FutureWarning: how in .resample() is deprecated
the new syntax is .resample(...).std()
|
machineID |
datetime |
voltmean_3h |
rotatemean_3h |
pressuremean_3h |
vibrationmean_3h |
| 0 |
1 |
2015-01-01 09:00:00 |
170.028993 |
449.533798 |
94.592122 |
40.893502 |
| 1 |
1 |
2015-01-01 12:00:00 |
164.192565 |
403.949857 |
105.687417 |
34.255891 |
| 2 |
1 |
2015-01-01 15:00:00 |
168.134445 |
435.781707 |
107.793709 |
41.239405 |
| 3 |
1 |
2015-01-01 18:00:00 |
165.514453 |
430.472823 |
101.703289 |
40.373739 |
| 4 |
1 |
2015-01-01 21:00:00 |
168.809347 |
437.111120 |
90.911060 |
41.738542 |
For capturing a longer term effect, 24 hour lag features are also calculated as below.
temp = []
fields = ['volt', 'rotate', 'pressure', 'vibration']
for col in fields:
temp.append(pd.rolling_mean(pd.pivot_table(telemetry,
index='datetime',
columns='machineID',
values=col), window=24).resample('3H',
closed='left',
label='right',
how='first').unstack())
telemetry_mean_24h = pd.concat(temp, axis=1)
telemetry_mean_24h.columns = [i + 'mean_24h' for i in fields]
telemetry_mean_24h.reset_index(inplace=True)
telemetry_mean_24h = telemetry_mean_24h.loc[-telemetry_mean_24h['voltmean_24h'].isnull()]
# repeat for standard deviation
temp = []
fields = ['volt', 'rotate', 'pressure', 'vibration']
for col in fields:
temp.append(pd.rolling_std(pd.pivot_table(telemetry,
index='datetime',
columns='machineID',
values=col), window=24).resample('3H',
closed='left',
label='right',
how='first').unstack())
telemetry_sd_24h = pd.concat(temp, axis=1)
telemetry_sd_24h.columns = [i + 'sd_24h' for i in fields]
telemetry_sd_24h = telemetry_sd_24h.loc[-telemetry_sd_24h['voltsd_24h'].isnull()]
telemetry_sd_24h.reset_index(inplace=True)
# Notice that a 24h rolling average is not available at the earliest timepoints
telemetry_mean_24h.head(10)
C:\Program Files\Microsoft\ML Server\PYTHON_SERVER\lib\site-packages\ipykernel_launcher.py:7: FutureWarning: pd.rolling_mean is deprecated for DataFrame and will be removed in a future version, replace with
DataFrame.rolling(window=24,center=False).mean()
import sys
C:\Program Files\Microsoft\ML Server\PYTHON_SERVER\lib\site-packages\ipykernel_launcher.py:10: FutureWarning: how in .resample() is deprecated
the new syntax is .resample(...).first()
# Remove the CWD from sys.path while we load stuff.
C:\Program Files\Microsoft\ML Server\PYTHON_SERVER\lib\site-packages\ipykernel_launcher.py:23: FutureWarning: pd.rolling_std is deprecated for DataFrame and will be removed in a future version, replace with
DataFrame.rolling(window=24,center=False).std()
C:\Program Files\Microsoft\ML Server\PYTHON_SERVER\lib\site-packages\ipykernel_launcher.py:26: FutureWarning: how in .resample() is deprecated
the new syntax is .resample(...).first()
|
machineID |
datetime |
voltmean_24h |
rotatemean_24h |
pressuremean_24h |
vibrationmean_24h |
| 7 |
1 |
2015-01-02 06:00:00 |
169.733809 |
445.179865 |
96.797113 |
40.385160 |
| 8 |
1 |
2015-01-02 09:00:00 |
170.614862 |
446.364859 |
96.849785 |
39.736826 |
| 9 |
1 |
2015-01-02 12:00:00 |
169.893965 |
447.009407 |
97.715600 |
39.498374 |
| 10 |
1 |
2015-01-02 15:00:00 |
171.243444 |
444.233563 |
96.666060 |
40.229370 |
| 11 |
1 |
2015-01-02 18:00:00 |
170.792486 |
448.440437 |
95.766838 |
40.055214 |
| 12 |
1 |
2015-01-02 21:00:00 |
170.556674 |
452.267095 |
98.065860 |
40.033247 |
| 13 |
1 |
2015-01-03 00:00:00 |
168.460525 |
451.031783 |
99.273286 |
38.903462 |
| 14 |
1 |
2015-01-03 03:00:00 |
169.772951 |
447.502464 |
99.005946 |
39.389725 |
| 15 |
1 |
2015-01-03 06:00:00 |
170.900562 |
453.864597 |
100.877342 |
38.696225 |
| 16 |
1 |
2015-01-03 09:00:00 |
169.533156 |
454.785072 |
100.050567 |
39.449734 |
Next, the columns of the feature datasets created earlier are merged to create the final feature set from telemetry.
# merge columns of feature sets created earlier
telemetry_feat = pd.concat([telemetry_mean_3h,
telemetry_sd_3h.ix[:, 2:6],
telemetry_mean_24h.ix[:, 2:6],
telemetry_sd_24h.ix[:, 2:6]], axis=1).dropna()
telemetry_feat.describe()
|
machineID |
voltmean_3h |
rotatemean_3h |
pressuremean_3h |
vibrationmean_3h |
voltsd_3h |
rotatesd_3h |
pressuresd_3h |
vibrationsd_3h |
voltmean_24h |
rotatemean_24h |
pressuremean_24h |
vibrationmean_24h |
voltsd_24h |
rotatesd_24h |
pressuresd_24h |
vibrationsd_24h |
| count |
290601.000000 |
290601.000000 |
290601.000000 |
290601.000000 |
290601.000000 |
290601.000000 |
290601.000000 |
290601.000000 |
290601.000000 |
290601.000000 |
290601.000000 |
290601.000000 |
290601.000000 |
290601.000000 |
290601.000000 |
290601.000000 |
290601.000000 |
| mean |
50.380935 |
170.774427 |
446.609386 |
100.858340 |
40.383609 |
13.300173 |
44.453951 |
8.885780 |
4.440575 |
170.775661 |
446.609874 |
100.857574 |
40.383881 |
14.919452 |
49.950788 |
10.046380 |
5.002089 |
| std |
28.798424 |
9.498824 |
33.119738 |
7.411701 |
3.475512 |
6.966389 |
23.214291 |
4.656364 |
2.319989 |
4.720237 |
18.070458 |
4.737293 |
2.058059 |
2.261097 |
7.684305 |
1.713206 |
0.799599 |
| min |
1.000000 |
125.532506 |
211.811184 |
72.118639 |
26.569635 |
0.025509 |
0.078991 |
0.027417 |
0.015278 |
155.812721 |
266.010419 |
91.057429 |
35.060087 |
6.380619 |
18.385248 |
4.145308 |
2.144863 |
| 25% |
25.000000 |
164.447794 |
427.564793 |
96.239534 |
38.147458 |
8.028675 |
26.906319 |
5.369959 |
2.684556 |
168.072275 |
441.542561 |
98.669734 |
39.354077 |
13.359069 |
44.669022 |
8.924165 |
4.460675 |
| 50% |
50.000000 |
170.432407 |
448.380260 |
100.235357 |
40.145874 |
12.495542 |
41.793798 |
8.345801 |
4.173704 |
170.212704 |
449.206885 |
100.099533 |
40.072618 |
14.854186 |
49.617459 |
9.921332 |
4.958793 |
| 75% |
75.000000 |
176.610017 |
468.443933 |
104.406534 |
42.226898 |
17.688520 |
59.092354 |
11.789358 |
5.898512 |
172.462228 |
456.366349 |
101.613047 |
40.833112 |
16.395372 |
54.826993 |
10.980250 |
5.484430 |
| max |
100.000000 |
241.420717 |
586.682904 |
162.309656 |
69.311324 |
58.444332 |
179.903039 |
35.659369 |
18.305595 |
220.782618 |
499.096975 |
152.310351 |
61.932124 |
27.664538 |
103.819404 |
28.654103 |
12.325783 |
telemetry_feat.head()
|
machineID |
datetime |
voltmean_3h |
rotatemean_3h |
pressuremean_3h |
vibrationmean_3h |
voltsd_3h |
rotatesd_3h |
pressuresd_3h |
vibrationsd_3h |
voltmean_24h |
rotatemean_24h |
pressuremean_24h |
vibrationmean_24h |
voltsd_24h |
rotatesd_24h |
pressuresd_24h |
vibrationsd_24h |
| 7 |
1 |
2015-01-02 06:00:00 |
180.133784 |
440.608320 |
94.137969 |
41.551544 |
21.322735 |
48.770512 |
2.135684 |
10.037208 |
169.733809 |
445.179865 |
96.797113 |
40.385160 |
15.726970 |
39.648116 |
11.904700 |
5.601191 |
| 8 |
1 |
2015-01-02 09:00:00 |
176.364293 |
439.349655 |
101.553209 |
36.105580 |
18.952210 |
51.329636 |
13.789279 |
6.737739 |
170.614862 |
446.364859 |
96.849785 |
39.736826 |
15.635083 |
41.828592 |
11.326412 |
5.583521 |
| 9 |
1 |
2015-01-02 12:00:00 |
160.384568 |
424.385316 |
99.598722 |
36.094637 |
13.047080 |
13.702496 |
9.988609 |
1.639962 |
169.893965 |
447.009407 |
97.715600 |
39.498374 |
13.995465 |
40.843882 |
11.036546 |
5.561553 |
| 10 |
1 |
2015-01-02 15:00:00 |
170.472461 |
442.933997 |
102.380586 |
40.483002 |
16.642354 |
56.290447 |
3.305739 |
8.854145 |
171.243444 |
444.233563 |
96.666060 |
40.229370 |
13.100364 |
43.409841 |
10.972862 |
6.068674 |
| 11 |
1 |
2015-01-02 18:00:00 |
163.263806 |
468.937558 |
102.726648 |
40.921802 |
17.424688 |
38.680380 |
9.105775 |
3.060781 |
170.792486 |
448.440437 |
95.766838 |
40.055214 |
13.808489 |
43.742304 |
10.988704 |
7.286129 |
Lag Features from Errors
Like telemetry data, errors come with timestamps. An important difference is that the error IDs are categorical values and should not be averaged over time intervals like the telemetry measurements. Instead, we count the number of errors of each type in a lagging window. We begin by reformatting the error data to have one entry per machine per time at which at least one error occurred:
errors
|
datetime |
machineID |
errorID |
| 0 |
2015-01-03 07:00:00 |
1 |
error1 |
| 1 |
2015-01-03 20:00:00 |
1 |
error3 |
| 2 |
2015-01-04 06:00:00 |
1 |
error5 |
| 3 |
2015-01-10 15:00:00 |
1 |
error4 |
| 4 |
2015-01-22 10:00:00 |
1 |
error4 |
| 5 |
2015-01-25 15:00:00 |
1 |
error4 |
| 6 |
2015-01-27 04:00:00 |
1 |
error1 |
| 7 |
2015-03-03 22:00:00 |
1 |
error2 |
| 8 |
2015-03-05 06:00:00 |
1 |
error1 |
| 9 |
2015-03-20 18:00:00 |
1 |
error1 |
| 10 |
2015-03-26 01:00:00 |
1 |
error2 |
| 11 |
2015-03-31 23:00:00 |
1 |
error1 |
| 12 |
2015-04-19 06:00:00 |
1 |
error2 |
| 13 |
2015-04-19 06:00:00 |
1 |
error3 |
| 14 |
2015-04-29 19:00:00 |
1 |
error4 |
| 15 |
2015-05-04 23:00:00 |
1 |
error2 |
| 16 |
2015-05-12 09:00:00 |
1 |
error1 |
| 17 |
2015-05-21 07:00:00 |
1 |
error4 |
| 18 |
2015-05-24 02:00:00 |
1 |
error3 |
| 19 |
2015-05-25 05:00:00 |
1 |
error1 |
| 20 |
2015-06-09 06:00:00 |
1 |
error3 |
| 21 |
2015-06-18 06:00:00 |
1 |
error5 |
| 22 |
2015-06-23 10:00:00 |
1 |
error3 |
| 23 |
2015-08-23 19:00:00 |
1 |
error1 |
| 24 |
2015-08-30 01:00:00 |
1 |
error3 |
| 25 |
2015-09-01 06:00:00 |
1 |
error5 |
| 26 |
2015-09-13 17:00:00 |
1 |
error2 |
| 27 |
2015-09-15 06:00:00 |
1 |
error1 |
| 28 |
2015-10-01 23:00:00 |
1 |
error1 |
| 29 |
2015-10-15 05:00:00 |
1 |
error1 |
| ... |
... |
... |
... |
| 3889 |
2015-01-16 00:00:00 |
100 |
error4 |
| 3890 |
2015-02-01 10:00:00 |
100 |
error1 |
| 3891 |
2015-02-11 06:00:00 |
100 |
error1 |
| 3892 |
2015-02-12 21:00:00 |
100 |
error1 |
| 3893 |
2015-03-08 15:00:00 |
100 |
error1 |
| 3894 |
2015-04-27 04:00:00 |
100 |
error4 |
| 3895 |
2015-04-27 22:00:00 |
100 |
error5 |
| 3896 |
2015-05-16 23:00:00 |
100 |
error2 |
| 3897 |
2015-05-17 13:00:00 |
100 |
error2 |
| 3898 |
2015-05-22 02:00:00 |
100 |
error3 |
| 3899 |
2015-07-05 16:00:00 |
100 |
error3 |
| 3900 |
2015-07-19 01:00:00 |
100 |
error2 |
| 3901 |
2015-08-14 16:00:00 |
100 |
error4 |
| 3902 |
2015-08-30 15:00:00 |
100 |
error4 |
| 3903 |
2015-09-09 06:00:00 |
100 |
error1 |
| 3904 |
2015-09-14 23:00:00 |
100 |
error3 |
| 3905 |
2015-10-03 05:00:00 |
100 |
error3 |
| 3906 |
2015-10-09 07:00:00 |
100 |
error1 |
| 3907 |
2015-10-17 02:00:00 |
100 |
error3 |
| 3908 |
2015-10-17 12:00:00 |
100 |
error1 |
| 3909 |
2015-10-24 23:00:00 |
100 |
error1 |
| 3910 |
2015-10-27 21:00:00 |
100 |
error2 |
| 3911 |
2015-11-05 02:00:00 |
100 |
error3 |
| 3912 |
2015-11-07 17:00:00 |
100 |
error1 |
| 3913 |
2015-11-12 01:00:00 |
100 |
error1 |
| 3914 |
2015-11-21 08:00:00 |
100 |
error2 |
| 3915 |
2015-12-04 02:00:00 |
100 |
error1 |
| 3916 |
2015-12-08 06:00:00 |
100 |
error2 |
| 3917 |
2015-12-08 06:00:00 |
100 |
error3 |
| 3918 |
2015-12-22 03:00:00 |
100 |
error3 |
3919 rows × 3 columns
# create a column for each error type
error_count = pd.get_dummies(errors.set_index('datetime')).reset_index()
error_count
error_count.columns = ['datetime', 'machineID', 'error1', 'error2', 'error3', 'error4', 'error5']
error_count.head(13)
|
datetime |
machineID |
error1 |
error2 |
error3 |
error4 |
error5 |
| 0 |
2015-01-03 07:00:00 |
1 |
1 |
0 |
0 |
0 |
0 |
| 1 |
2015-01-03 20:00:00 |
1 |
0 |
0 |
1 |
0 |
0 |
| 2 |
2015-01-04 06:00:00 |
1 |
0 |
0 |
0 |
0 |
1 |
| 3 |
2015-01-10 15:00:00 |
1 |
0 |
0 |
0 |
1 |
0 |
| 4 |
2015-01-22 10:00:00 |
1 |
0 |
0 |
0 |
1 |
0 |
| 5 |
2015-01-25 15:00:00 |
1 |
0 |
0 |
0 |
1 |
0 |
| 6 |
2015-01-27 04:00:00 |
1 |
1 |
0 |
0 |
0 |
0 |
| 7 |
2015-03-03 22:00:00 |
1 |
0 |
1 |
0 |
0 |
0 |
| 8 |
2015-03-05 06:00:00 |
1 |
1 |
0 |
0 |
0 |
0 |
| 9 |
2015-03-20 18:00:00 |
1 |
1 |
0 |
0 |
0 |
0 |
| 10 |
2015-03-26 01:00:00 |
1 |
0 |
1 |
0 |
0 |
0 |
| 11 |
2015-03-31 23:00:00 |
1 |
1 |
0 |
0 |
0 |
0 |
| 12 |
2015-04-19 06:00:00 |
1 |
0 |
1 |
0 |
0 |
0 |
# combine errors for a given machine in a given hour
error_count = error_count.groupby(['machineID','datetime']).sum().reset_index()
error_count.head(13)
|
machineID |
datetime |
error1 |
error2 |
error3 |
error4 |
error5 |
| 0 |
1 |
2015-01-03 07:00:00 |
1 |
0 |
0 |
0 |
0 |
| 1 |
1 |
2015-01-03 20:00:00 |
0 |
0 |
1 |
0 |
0 |
| 2 |
1 |
2015-01-04 06:00:00 |
0 |
0 |
0 |
0 |
1 |
| 3 |
1 |
2015-01-10 15:00:00 |
0 |
0 |
0 |
1 |
0 |
| 4 |
1 |
2015-01-22 10:00:00 |
0 |
0 |
0 |
1 |
0 |
| 5 |
1 |
2015-01-25 15:00:00 |
0 |
0 |
0 |
1 |
0 |
| 6 |
1 |
2015-01-27 04:00:00 |
1 |
0 |
0 |
0 |
0 |
| 7 |
1 |
2015-03-03 22:00:00 |
0 |
1 |
0 |
0 |
0 |
| 8 |
1 |
2015-03-05 06:00:00 |
1 |
0 |
0 |
0 |
0 |
| 9 |
1 |
2015-03-20 18:00:00 |
1 |
0 |
0 |
0 |
0 |
| 10 |
1 |
2015-03-26 01:00:00 |
0 |
1 |
0 |
0 |
0 |
| 11 |
1 |
2015-03-31 23:00:00 |
1 |
0 |
0 |
0 |
0 |
| 12 |
1 |
2015-04-19 06:00:00 |
0 |
1 |
1 |
0 |
0 |
error_count = telemetry[['datetime', 'machineID']].merge(error_count, on=['machineID', 'datetime'], how='left').fillna(0.0)
error_count.describe()
|
machineID |
error1 |
error2 |
error3 |
error4 |
error5 |
| count |
876100.000000 |
876100.000000 |
876100.000000 |
876100.000000 |
876100.000000 |
876100.000000 |
| mean |
50.500000 |
0.001153 |
0.001128 |
0.000957 |
0.000830 |
0.000406 |
| std |
28.866087 |
0.033934 |
0.033563 |
0.030913 |
0.028795 |
0.020154 |
| min |
1.000000 |
0.000000 |
0.000000 |
0.000000 |
0.000000 |
0.000000 |
| 25% |
25.750000 |
0.000000 |
0.000000 |
0.000000 |
0.000000 |
0.000000 |
| 50% |
50.500000 |
0.000000 |
0.000000 |
0.000000 |
0.000000 |
0.000000 |
| 75% |
75.250000 |
0.000000 |
0.000000 |
0.000000 |
0.000000 |
0.000000 |
| max |
100.000000 |
1.000000 |
1.000000 |
1.000000 |
1.000000 |
1.000000 |
Finally, we can compute the total number of errors of each type over the last 24 hours, for timepoints taken every three hours:
temp = []
fields = ['error%d' % i for i in range(1,6)]
for col in fields:
temp.append(pd.rolling_sum(pd.pivot_table(error_count,
index='datetime',
columns='machineID',
values=col), window=24).resample('3H',
closed='left',
label='right',
how='first').unstack())
error_count = pd.concat(temp, axis=1)
error_count.columns = [i + 'count' for i in fields]
error_count.reset_index(inplace=True)
error_count = error_count.dropna()
error_count.describe()
C:\Program Files\Microsoft\ML Server\PYTHON_SERVER\lib\site-packages\ipykernel_launcher.py:7: FutureWarning: pd.rolling_sum is deprecated for DataFrame and will be removed in a future version, replace with
DataFrame.rolling(window=24,center=False).sum()
import sys
C:\Program Files\Microsoft\ML Server\PYTHON_SERVER\lib\site-packages\ipykernel_launcher.py:10: FutureWarning: how in .resample() is deprecated
the new syntax is .resample(...).first()
# Remove the CWD from sys.path while we load stuff.
|
machineID |
error1count |
error2count |
error3count |
error4count |
error5count |
| count |
291400.00000 |
291400.000000 |
291400.000000 |
291400.000000 |
291400.000000 |
291400.000000 |
| mean |
50.50000 |
0.027649 |
0.027069 |
0.022907 |
0.019904 |
0.009753 |
| std |
28.86612 |
0.166273 |
0.164429 |
0.151453 |
0.140820 |
0.098797 |
| min |
1.00000 |
0.000000 |
0.000000 |
0.000000 |
0.000000 |
0.000000 |
| 25% |
25.75000 |
0.000000 |
0.000000 |
0.000000 |
0.000000 |
0.000000 |
| 50% |
50.50000 |
0.000000 |
0.000000 |
0.000000 |
0.000000 |
0.000000 |
| 75% |
75.25000 |
0.000000 |
0.000000 |
0.000000 |
0.000000 |
0.000000 |
| max |
100.00000 |
2.000000 |
2.000000 |
2.000000 |
2.000000 |
2.000000 |
error_count.head()
|
machineID |
datetime |
error1count |
error2count |
error3count |
error4count |
error5count |
| 7 |
1 |
2015-01-02 06:00:00 |
0.0 |
0.0 |
0.0 |
0.0 |
0.0 |
| 8 |
1 |
2015-01-02 09:00:00 |
0.0 |
0.0 |
0.0 |
0.0 |
0.0 |
| 9 |
1 |
2015-01-02 12:00:00 |
0.0 |
0.0 |
0.0 |
0.0 |
0.0 |
| 10 |
1 |
2015-01-02 15:00:00 |
0.0 |
0.0 |
0.0 |
0.0 |
0.0 |
| 11 |
1 |
2015-01-02 18:00:00 |
0.0 |
0.0 |
0.0 |
0.0 |
0.0 |
Days Since Last Replacement from Maintenance
A crucial data set in this example is the maintenance records which contain the information of component replacement records. Possible features from this data set can be, for example, the number of replacements of each component in the last 3 months to incorporate the frequency of replacements. However, more relevent information would be to calculate how long it has been since a component is last replaced as that would be expected to correlate better with component failures since the longer a component is used, the more degradation should be expected.
As a side note, creating lagging features from maintenance data is not as straightforward as for telemetry and errors, so the features from this data are generated in a more custom way. This type of ad-hoc feature engineering is very common in predictive maintenance since domain knowledge plays a big role in understanding the predictors of a problem. In the following, the days since last component replacement are calculated for each component type as features from the maintenance data.
import numpy as np
# create a column for each error type
comp_rep = pd.get_dummies(maint.set_index('datetime')).reset_index()
comp_rep.columns = ['datetime', 'machineID', 'comp1', 'comp2', 'comp3', 'comp4']
# combine repairs for a given machine in a given hour
comp_rep = comp_rep.groupby(['machineID', 'datetime']).sum().reset_index()
# add timepoints where no components were replaced
comp_rep = telemetry[['datetime', 'machineID']].merge(comp_rep,
on=['datetime', 'machineID'],
how='outer').fillna(0).sort_values(by=['machineID', 'datetime'])
components = ['comp1', 'comp2', 'comp3', 'comp4']
for comp in components:
# convert indicator to most recent date of component change
comp_rep.loc[comp_rep[comp] < 1, comp] = None
comp_rep.loc[-comp_rep[comp].isnull(), comp] = comp_rep.loc[-comp_rep[comp].isnull(), 'datetime']
# forward-fill the most-recent date of component change
comp_rep[comp] = comp_rep[comp].fillna(method='ffill')
# remove dates in 2014 (may have NaN or future component change dates)
comp_rep = comp_rep.loc[comp_rep['datetime'] > pd.to_datetime('2015-01-01')]
# replace dates of most recent component change with days since most recent component change
for comp in components:
comp_rep[comp] = (comp_rep['datetime'] - comp_rep[comp]) / np.timedelta64(1, 'D')
comp_rep.describe()
|
machineID |
comp1 |
comp2 |
comp3 |
comp4 |
| count |
876100.000000 |
876100.000000 |
876100.000000 |
876100.000000 |
876100.000000 |
| mean |
50.500000 |
53.525185 |
51.540806 |
52.725962 |
53.834191 |
| std |
28.866087 |
62.491679 |
59.269254 |
58.873114 |
59.707978 |
| min |
1.000000 |
0.000000 |
0.000000 |
0.000000 |
0.000000 |
| 25% |
25.750000 |
13.291667 |
12.125000 |
13.125000 |
13.000000 |
| 50% |
50.500000 |
32.791667 |
29.666667 |
32.291667 |
32.500000 |
| 75% |
75.250000 |
68.708333 |
66.541667 |
67.333333 |
70.458333 |
| max |
100.000000 |
491.958333 |
348.958333 |
370.958333 |
394.958333 |
comp_rep.head()
|
datetime |
machineID |
comp1 |
comp2 |
comp3 |
comp4 |
| 0 |
2015-01-01 06:00:00 |
1 |
19.000000 |
214.000000 |
154.000000 |
169.000000 |
| 1 |
2015-01-01 07:00:00 |
1 |
19.041667 |
214.041667 |
154.041667 |
169.041667 |
| 2 |
2015-01-01 08:00:00 |
1 |
19.083333 |
214.083333 |
154.083333 |
169.083333 |
| 3 |
2015-01-01 09:00:00 |
1 |
19.125000 |
214.125000 |
154.125000 |
169.125000 |
| 4 |
2015-01-01 10:00:00 |
1 |
19.166667 |
214.166667 |
154.166667 |
169.166667 |
Machine Features
The machine features can be used without further modification. These include descriptive information about the type of each machine and its age (number of years in service). If the age information had been recorded as a “first use date” for each machine, a transformation would have been necessary to turn those into a numeric values indicating the years in service.
Lastly, we merge all the feature data sets we created earlier to get the final feature matrix.
telemetry_feat
|
machineID |
datetime |
voltmean_3h |
rotatemean_3h |
pressuremean_3h |
vibrationmean_3h |
voltsd_3h |
rotatesd_3h |
pressuresd_3h |
vibrationsd_3h |
voltmean_24h |
rotatemean_24h |
pressuremean_24h |
vibrationmean_24h |
voltsd_24h |
rotatesd_24h |
pressuresd_24h |
vibrationsd_24h |
| 7 |
1 |
2015-01-02 06:00:00 |
180.133784 |
440.608320 |
94.137969 |
41.551544 |
21.322735 |
48.770512 |
2.135684 |
10.037208 |
169.733809 |
445.179865 |
96.797113 |
40.385160 |
15.726970 |
39.648116 |
11.904700 |
5.601191 |
| 8 |
1 |
2015-01-02 09:00:00 |
176.364293 |
439.349655 |
101.553209 |
36.105580 |
18.952210 |
51.329636 |
13.789279 |
6.737739 |
170.614862 |
446.364859 |
96.849785 |
39.736826 |
15.635083 |
41.828592 |
11.326412 |
5.583521 |
| 9 |
1 |
2015-01-02 12:00:00 |
160.384568 |
424.385316 |
99.598722 |
36.094637 |
13.047080 |
13.702496 |
9.988609 |
1.639962 |
169.893965 |
447.009407 |
97.715600 |
39.498374 |
13.995465 |
40.843882 |
11.036546 |
5.561553 |
| 10 |
1 |
2015-01-02 15:00:00 |
170.472461 |
442.933997 |
102.380586 |
40.483002 |
16.642354 |
56.290447 |
3.305739 |
8.854145 |
171.243444 |
444.233563 |
96.666060 |
40.229370 |
13.100364 |
43.409841 |
10.972862 |
6.068674 |
| 11 |
1 |
2015-01-02 18:00:00 |
163.263806 |
468.937558 |
102.726648 |
40.921802 |
17.424688 |
38.680380 |
9.105775 |
3.060781 |
170.792486 |
448.440437 |
95.766838 |
40.055214 |
13.808489 |
43.742304 |
10.988704 |
7.286129 |
| 12 |
1 |
2015-01-02 21:00:00 |
163.278466 |
446.493166 |
104.387585 |
38.068116 |
21.580492 |
41.380958 |
20.725597 |
6.932127 |
170.556674 |
452.267095 |
98.065860 |
40.033247 |
14.187985 |
40.676672 |
11.942227 |
8.723238 |
| 13 |
1 |
2015-01-03 00:00:00 |
172.191198 |
434.214692 |
93.747282 |
39.716482 |
16.369836 |
14.636041 |
18.817326 |
3.426997 |
168.460525 |
451.031783 |
99.273286 |
38.903462 |
13.707794 |
40.509184 |
10.141026 |
8.634082 |
| 14 |
1 |
2015-01-03 03:00:00 |
175.210027 |
504.845430 |
108.512153 |
37.763933 |
5.991921 |
16.062702 |
6.382608 |
3.449468 |
169.772951 |
447.502464 |
99.005946 |
39.389725 |
11.818603 |
44.468516 |
9.444955 |
8.332673 |
| 15 |
1 |
2015-01-03 06:00:00 |
181.690108 |
472.783187 |
93.395164 |
38.621099 |
11.514450 |
47.880443 |
2.177029 |
7.670520 |
170.900562 |
453.864597 |
100.877342 |
38.696225 |
12.069391 |
46.669661 |
8.609526 |
8.089348 |
| 16 |
1 |
2015-01-03 09:00:00 |
172.382935 |
505.141261 |
98.524373 |
49.965572 |
7.065150 |
56.849540 |
5.230039 |
2.687565 |
169.533156 |
454.785072 |
100.050567 |
39.449734 |
12.755234 |
44.016114 |
9.893704 |
7.013132 |
| 17 |
1 |
2015-01-03 12:00:00 |
174.303858 |
436.182686 |
94.092681 |
50.999589 |
19.017196 |
26.420163 |
7.661944 |
3.516734 |
170.866013 |
463.871291 |
99.360632 |
40.766639 |
12.848646 |
45.090576 |
9.846662 |
5.888262 |
| 18 |
1 |
2015-01-03 15:00:00 |
176.246348 |
451.646684 |
98.102389 |
59.198241 |
12.572504 |
31.574383 |
15.559351 |
6.562087 |
171.041651 |
463.701291 |
98.965877 |
42.396850 |
14.968351 |
37.088898 |
10.133452 |
5.702356 |
| 19 |
1 |
2015-01-03 18:00:00 |
158.433533 |
453.900213 |
98.878129 |
46.851925 |
5.136952 |
21.216569 |
11.400650 |
2.688559 |
171.244533 |
464.320613 |
98.853189 |
44.608814 |
17.058217 |
36.617908 |
9.867174 |
5.743753 |
| 20 |
1 |
2015-01-03 21:00:00 |
162.387954 |
454.140377 |
92.651129 |
54.261635 |
4.563331 |
57.747656 |
4.754203 |
5.118076 |
171.385039 |
459.937314 |
97.292157 |
45.284751 |
18.405763 |
35.819938 |
9.743769 |
5.246435 |
| 21 |
1 |
2015-01-04 00:00:00 |
174.243192 |
394.998095 |
99.829845 |
46.930738 |
6.268730 |
29.167663 |
10.564287 |
6.822855 |
171.880633 |
461.437128 |
96.786742 |
47.311018 |
18.249831 |
42.055638 |
10.961128 |
5.093464 |
| 22 |
1 |
2015-01-04 03:00:00 |
176.443361 |
459.528820 |
111.855296 |
55.296056 |
16.330285 |
20.602657 |
7.064583 |
4.651468 |
172.513202 |
456.429165 |
97.742700 |
48.416442 |
19.141287 |
37.018824 |
10.642956 |
4.618287 |
| 23 |
1 |
2015-01-04 06:00:00 |
186.092896 |
451.641253 |
107.989359 |
55.308074 |
13.489090 |
62.185045 |
5.118176 |
4.904365 |
172.686245 |
453.387589 |
99.304019 |
51.158654 |
18.887033 |
36.997459 |
11.042775 |
5.195423 |
| 24 |
1 |
2015-01-04 |
README 内容过长,已截取前 64 KB。完整内容请前往 GitHub 仓库查看。
评论