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: runTime can be used to estimate how long the vehicle was operating. This code, which adjusts for missing records between values of runTime of 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), on runTime per day, the above code can be run in a loop as in, where df_aggs will 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 runTime

  • gdf['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, runTime jumps 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 runTime
  • This 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
Example where runTime is null intermittently
Histogram of Average Daily Engine Operating Hours (sum of hours the vehicle was operating / number of days the vehicle was operating)
Histogram of Average Daily Engine Operating Hours (average of the hours of vehicle operation per day, limited to days when the vehicle ran)

Last updated