runTime
The amount of time that the engine has been running (for internal combustion vehicles)
High-Level Explanation
This signal tracks how long the engine has been running since the engine was most recently turned on, in seconds.
This signal stops increasing and remains at the maximum value for that trip when the engine is turned off (though data is still being collected for a brief time after the engine shuts off).
The signal also starts at 0 when the engine is turned on and starts increasing as the engine remains on. The values remain constant once the engine is turned off, then resets to zero (and again starts incrementing) when the engine is turned back on, unlike enginetotalhoursofoperationj1939 - SPN 247which always increments and never resets, similar to odometer.
Enables
Total Vehicle Operation Time:
runTimecan be used to estimate how long the vehicle was operating. This code, which adjusts for missing records between values ofrunTimeof can be used to estimate total engine operating time per day:gdf = gdf.dropna(subset = 'runTime').reset_index(drop = True) gdf['timestamp_diff_seconds'] = gdf['timestamp'].diff()/np.timedelta64(1,'s') gdf['year_month_day'] = gdf['timestamp'].map(lambda x: x.strftime('%Y_%m_%d')) gdf['runTime_diff'] = gdf['runTime'].diff() gdf['runTime_reset'] = (gdf['runTime_diff'] < 0) | ((gdf['timestamp_diff_seconds'] > 2*gdf['runTime_diff']) & (gdf['timestamp_diff_seconds'] > 600)) gdf['runTime_trip'] = gdf['runTime_reset'].cumsum() gb = gdf.groupby('runTime_trip').agg({'runTime':np.max,'year_month_day':['first','last'],'timestamp':['first','last']}) gb.columns = [i[0] + '_' + i[1] for i in gb.columns]To determine fleet-wide aggregated statistics, such as average operating hours per day (
avg_operating_hours_per_day) and fraction of days for which the vehicle was running (fraction_of_days_with_operation), onrunTimeper day, the above code can be run in a loop as in, wheredf_aggswill contain the aggregated data with the vehicle subjects as the index:for subject in subjects: gdf = get_data_for_subject(subject) # Run above code gb['subject'] = subject df_lst.append(gb) df_runtime = pd.concat(df_lst) for sub in df_runtime['subject'].unique(): try: frac = df_runtime.loc[df_runtime['subject'] == sub]['first_datetime'].nunique()/((df_runtime.loc[df_runtime['subject'] == sub]['first_datetime'].max() - df_runtime.loc[df_runtime['subject'] == sub]['first_datetime'].min())/(np.timedelta64(1,'D'))) except ZeroDivisionError: frac = np.nan fracs.append(frac) avg = df_runtime.loc[df_runtime['subject'] == sub].groupby('first_datetime')['runTime_amax'].sum().mean()/(60**2) avgs.append(avg) df_aggs = pd.DataFrame(index = df_runtime['subject'].unique()) df_aggs['avg_operating_hours_per_day'] = avgs df_aggs['fraction_of_days_with_operation'] = fracs
Note that some trips start in one day and end in the next. That is, part of the running time should be attributed to the first day and part to the next. In order to properly capture the running time per day,
temp = gb.loc[gb['year_month_day_first'] != gb['year_month_day_last']]can identify these split trips and the following code can be used to unsplit them (i.e. take the portion of engine runtime that occured during each day and attribute it to that day)temp['midnight_last_day'] = pd.to_datetime(temp[['timestamp_first','year_month_day_last']].apply(lambda x: x['year_month_day_last'] + ' 00:00:00' + x['timestamp_first'].strftime('%z'), axis = 1), format = '%Y_%m_%d %H:%M:%S%z') temp['year_month_day_first'] = pd.to_datetime(temp['year_month_day_first'], format = '%Y_%m_%d') temp2 = temp.copy(deep = True) temp2['runTime_total_hours'] = (temp2['midnight_last_day'] - temp2['timestamp_first'])/np.timedelta64(1,'h') temp['runTime_total_hours'] = temp['runTime_total_hours'] - (temp2['midnight_last_day'] - temp2['timestamp_first'])/np.timedelta64(1,'h') temp['year_month_day_first'] = temp['year_month_day_last'] temp2['year_month_day_first'] = temp2['year_month_day_first'].map(lambda x: x.strftime('%Y_%m_%d')) temp2['year_month_day_last'] = temp2['year_month_day_first'] gb = pd.concat((gb.loc[~gb.index.isin(temp.index)],pd.concat((temp,temp2))))
Trip identification: it is fairly reasonable to assume that when the engine stops, a trip has completed. This code, while assuming no missing data, can be used to identify trips based on resets in
runTimegdf['runTime_diff'] = gdf['runTime'].ffill().bfill().diff() gdf['runTime_diff_trip_id'] = gdf['runTime_diff'] < 0 gdf['runTime_diff_trip_id'] = gdf['runTime_diff_trip_id'].cumsum()
Enabled By
This signal (and usefulness for identifying trips) only exists for internal combustion engines
Known Quirks
Note that for about 1% of vehicles,
runTimejumps up to an extremely high value and back down instead of monotonically increasing during trips and resetting back to zero when the engine stops and starts again.
Erroneous values of runTimeThis signal is null frequently. For an example vehicle, it is null for 65% of the records. As shown in an example below, this signal is frequently missing intermittently.
Visualizations with Explanations

runTime incrementing and resetting to zero when the engine turns off and back on
runTime is null intermittently

Last updated