← Back to list

Travel Time Analysis Project Series 08: Modeling — Feature Selection

Selecting key features to enhance model performance and efficiency

Wei Hao Huang · 2025-04-18 13:02 · 5 claps · 10.2 min read
#feature-selection #auto-feature-engineering #random-forest #travel-time-analysis #time-series-analysis
Open on Medium ↗
Wiki topics: 🏛️ · Politics ✈️ · Travel

Travel Time Analysis Project Series 08: Modeling — Feature Selection

Image by Gordon Johnson from Pixabay

Image by Gordon Johnson from Pixabay

[English / 中文] If you are not a member you can read the story here.

This is the eighth article in the Travel Time Analysis Project series. The goal of this project is to leverage data related to accidents, road construction, holidays, and other relevant factors to conduct time series analysis and forecasting. Our aim is to identify the most suitable neural network model for predicting travel times on specific segments of Taiwan’s national highways in designated directions. If you’re interested in learning more about the project’s origins and initial concepts, please refer to the first article in the series: Travel Time Analysis Project Series 01: Overview and Introduction.

Why Is Feature Selection Necessary?

In theory, we can feed all available variables directly into a deep learning model. These models are designed to learn patterns automatically, and in domains like image or speech recognition, this “throw everything in” strategy can work well. However, it also comes with some serious downsides.

When the number of features grows large, training becomes more expensive — both in time and computational resources. It also increases the risk of overfitting, especially when the amount of data isn’t enough to support that many features. On top of that, many variables might be highly correlated or redundant, making it harder for the model to focus on what truly matters.

By applying feature selection before training, we can filter out less informative variables, reduce the dimensionality of the data, and speed up the training process. This not only helps the model generalize better to new data, but also improves interpretability — giving us clearer insights into what drives the predictions. Even in deep learning workflows, thoughtful feature selection remains a valuable and often necessary step.

Feature Selection Methods

Feature selection methods generally fall into three categories: Filter, Wrapper, and Embedded approaches.

Filter Method

  • Performed independently of any model, this method ranks and selects features based on their statistical relationship with the target variable.
  • Fast and model-agnostic, it is suitable for initial screening. However, some domain knowledge is often needed to make informed decisions.
  • Common techniques include:
  • Correlation coefficients: such as Pearson or Spearman correlation.
  • Univariate selection: such as Chi-square test or ANOVA F-test.
  • Variance thresholding: removes features with very low variance (i.e., nearly constant).
  • Mutual information: quantifies the amount of shared information between variables.

Wrapper Method

  • This method evaluates different combinations of features by training a model on each subset and selecting the best-performing group.
  • It takes feature interactions into account and often yields better results.
  • However, it is computationally expensive — especially when dealing with many features — as each feature subset requires model training and evaluation. The process can be time-consuming and resource-intensive.
  • Common techniques include:
  • Recursive Feature Elimination (RFE): iteratively removes the least important features based on model performance.
  • Forward/Backward Selection: starts with an empty set or full set of features and adds or removes one feature at a time, evaluating model performance at each step.

Embedded Method

  • Feature selection is built into the model training process itself, using the model’s internal weights or structure to assess feature importance.
  • It strikes a good balance between efficiency and accuracy, making it suitable for high-dimensional datasets.
  • Common techniques include:
  • L1 regularization (Lasso Regression): automatically drives unimportant feature weights to zero.
  • Tree-based models (e.g., Random Forest, XGBoost): inherently provide feature importance metrics.
  • Elastic Net: combines L1 and L2 regularization to balance feature selection and model stability.

In this project, I ultimately chose the Embedded Method, implemented via Random Forest. One of the key advantages is that feature importance scores derived from Random Forests are interpretable, helping us understand which gantry pairs most strongly influence travel time.

Implementation

In this stage, we once again leveraged the previously developed hwttp toolkit. Since we needed to extract and inspect all features, hwttp was especially useful thanks to its built-in feature combinations. The following code is organized into three parts:

  • Data Loading: We load the cleaned dataset and integrate additional information such as accidents, congestion time by segment, and holiday indicators based on defined relationships.
  • Feature Composition: The hwttp.hwtoolkit module includes prebuilt methods for joining and combining variables.
  • Feature Importance and Selection: Using Random Forest to compute feature importance, which is the core focus of this analysis.
import os
import sys
import time
from pathlib import Path
from os import listdir
from os.path import isfile, join

import numpy as np
import pandas as pd
from tqdm import tqdm
import matplotlib.pyplot as plt

from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import TimeSeriesSplit

sys.path.append(os.path.abspath('..'))
import hwttp.hwtoolkit as tk

Load Data

data_paths = {'etag_5n_loc': '../data/cleaned/etag_5n_loc.csv',
              'section_info': '../data/cleaned/section_info.csv',
              'hw5_m04a_df': '../data/cleaned/hw5_m04a.csv',
              'congestion_table': '../data/cleaned/congestion_table.csv',
              'calendar_event': '../data/cleaned/calendar_event.csv',
              'road_build_event': '../data/cleaned/202301_10_road_build_event.xlsx',
              'traffic_accident_data': '../data/cleaned/202301_10_traffic_accident_data.xlsx'
             }

rs = tk.hw_df_resource(data_paths)
rs.load_raw_environment_info()
rs.load_raw_event_info()
rs.generate_mile_location_info()

# remove irrelevant holidays
drop_event_list = ['婦女節', '愚人節', '復活節', '地球日', '感恩節', 'Black Friday', 'Cyber Monday', '冬至']
rs.calendar_event = rs.calendar_event[~rs.calendar_event.event_name.isin(drop_event_list)].copy()

# Travel time data
hw5_15watt = pd.read_csv("../data/features/hw5_15watt.csv")
hw5_15watt = hw5_15watt[~hw5_15watt.gf_gt.isin(['03F0150N-03F0140N', 
                                                '03F0201S-03A0041N', 
                                                '03F0201S-03F0217S'])].copy()
hw5_15watt = hw5_15watt[hw5_15watt.TimeStamp<'2024-01-01 00:00:00'].copy()
hw5_15watt['TimeStamp'] = pd.to_datetime(hw5_15watt['TimeStamp'])

At this point, the core dataset contains 280,320 rows and 6 columns. No additional derived variables have been appended yet.

<class 'pandas.core.frame.DataFrame'>
Int64Index: 280320 entries, 131328 to 472799
Data columns (total 6 columns):
 #   Column                 Non-Null Count   Dtype         
---  ------                 --------------   -----         
 0   gf_gt                  280320 non-null  object        
 1   GantryFrom             280320 non-null  object        
 2   GantryTo               280320 non-null  object        
 3   TimeStamp              280320 non-null  datetime64[ns]
 4   WeightedAvgTravelTime  280320 non-null  float64       
 5   TotalTraffic           280320 non-null  float64   

Combinations

In the initial experiments, I opted for a wrapper method — Recursive Feature Elimination (RFE) — to train and validate the model. However, after feeding in a large number of features, it became clear that the method was impractical; each training iteration took far too long to complete.

To address this, I defined function-based handling to systematically expand the feature set. At this stage, all available feature types are included. Below are the simple letter codes I used to represent each type, which together form the full set of features used to estimate travel time between gantries:

  • c: Congestion duration for the corresponding road segment
  • h: Holiday indicators
  • t: Traffic accident information
  • r: Road construction data
  • p: Travel time from the downstream segment in the previous five minutes
# function of appending features to source dataframe 
def add_c(df):
    output_df = tk.add_congestion_condition(df, rs.congestion_table, rs.milelocation_info_df).copy()
    return output_df

def add_h(df):
    output_df = tk.add_calendar_event(df, rs.calendar_event).copy()
    return output_df

def add_t(df):
    *_, output_df = tk.add_traffic_event(df, rs.traffic_accident_data, rs.milelocation_info_df)
    return output_df

def add_r(df):
    *_, output_df = tk.add_road_build_event(df, rs.road_build_event, rs.milelocation_info_df)
    return output_df

def add_p(df):
    output_df = tk.add_ds_5prev_traveltime(df)
    return output_df

# put function into the dictionary
element_dict = {'_c': add_c, 
                '_h': add_h, 
                '_t': add_t, 
                '_r': add_r, 
                '_p': add_p
               }

# combinations for gantry pair
combinations = {'05F0001N-03F0150N': ['_c', '_h', '_t', '_r', '_p'],
                '05F0001N-03F0201S': ['_c', '_h', '_t', '_r', '_p'],
                '05F0055N-05F0001N': ['_c', '_h', '_t', '_r', '_p'],
                '05F0287N-05F0055N': ['_c', '_h', '_t', '_r', '_p'],
                '05F0309N-05F0287N': ['_c', '_h', '_t', '_r', '_p'],
                '05F0438N-05F0309N': ['_c', '_h', '_t', '_r', '_p'],
                '05F0438N-05FR143N': ['_c', '_h', '_t', '_r', '_p'],
                '05F0528N-05F0438N': ['_c', '_h', '_t', '_r', '_p'],
               }

Due to the way the data structure was designed, the process of attaching features to the travel time records wasn’t as smooth as expected. Specifically, upstream and downstream-related features had to be merged first before other types could be appended properly.

Each feature-adding step can be executed by calling the corresponding function from the previously defined element_dict, making the process modular and easy to manage.

# dict to store results
results = dict()

# implement all the combinations
for gantry_pair, combo in tqdm(combinations.items()):
    # initialization
    result_df = hw5_15watt.copy()
    output_string = gantry_pair+'_b'
    print(gantry_pair, combo)
    combo_list = combo.copy()

    # bad handling, need to join _p feature first, or multiple columns will appeared.
    if '_p' in combo_list:
        combo_list.remove('_p')
        func = element_dict['_p']
        result_df = func(result_df)
        output_string += '_p'

    print('combo_list = ', combo_list)

    # join rest features from the combinations
    for func_name in combo_list:
        func = element_dict[func_name]
        result_df = func(result_df)
        output_string += func_name

    # at last, limit to specific gantry pair
    result_df = result_df[result_df['gf_gt']==gantry_pair].copy()

    # save to the results dict
    results[output_string] = result_df.copy()

This is one of the completed data tables, containing a total of 63 columns.

<class 'pandas.core.frame.DataFrame'>
Int64Index: 35040 entries, 0 to 35039
Data columns (total 63 columns):
 #   Column                           Non-Null Count  Dtype         
---  ------                           --------------  -----         
 0   gf_gt                            35040 non-null  object        
 1   GantryFrom                       35040 non-null  object        
 2   GantryTo                         35040 non-null  object        
 3   TimeStamp                        35040 non-null  datetime64[ns]
 4   WeightedAvgTravelTime            35040 non-null  float64       
 5   TotalTraffic                     35040 non-null  float64       
 6   ds_prev_1_WATT                   35040 non-null  float64       
 7   ds_prev_2_WATT                   35040 non-null  float64       
 8   ds_prev_3_WATT                   35040 non-null  float64       
 9   ds_prev_4_WATT                   35040 non-null  float64       
 10  ds_prev_5_WATT                   35040 non-null  float64       
 11  congestion_syndrome              35040 non-null  int64         
 12  holiday_continue                 35040 non-null  int64         
 13  holiday_length                   35040 non-null  float64       
 14  dayofweek                        35040 non-null  int64         
 15  holiday_name                     4416 non-null   object        
 16  holiday_name_七夕情人節               35040 non-null  int64         
 17  holiday_name_中元節                 35040 non-null  int64         
 18  holiday_name_中秋節                 35040 non-null  int64         
 19  holiday_name_二二八紀念日              35040 non-null  int64         
 20  holiday_name_元宵節                 35040 non-null  int64         
 21  holiday_name_兒童與清明節              35040 non-null  int64         
 22  holiday_name_勞動節                 35040 non-null  int64         
 23  holiday_name_國慶日                 35040 non-null  int64         
 24  holiday_name_教師節                 35040 non-null  int64         
 25  holiday_name_母親節                 35040 non-null  int64         
 26  holiday_name_父親節                 35040 non-null  int64         
 27  holiday_name_白色情人節               35040 non-null  int64         
 28  holiday_name_端午節                 35040 non-null  int64         
 29  holiday_name_聖誕節                 35040 non-null  int64         
 30  holiday_name_萬聖節                 35040 non-null  int64         
 31  holiday_name_西洋情人節               35040 non-null  int64         
 32  holiday_name_跨年元旦                35040 non-null  int64         
 33  holiday_name_農曆新年                35040 non-null  int64         
 34  accident_mileage                 35040 non-null  float64       
 35  event_occurrence                 35040 non-null  object        
 36  event_exclusion                  35040 non-null  object        
 37  handling_minutes                 35040 non-null  float64       
 38  accident_type                    35040 non-null  int64         
 39  death_count                      35040 non-null  float64       
 40  injuries_count                   35040 non-null  float64       
 41  inner_shoulder_flag              35040 non-null  float64       
 42  inner_lane_flag                  35040 non-null  float64       
 43  middle_inner_lane_flag           35040 non-null  float64       
 44  middle_lane_flag                 35040 non-null  float64       
 45  middle_outer_lane_flag           35040 non-null  float64       
 46  outer_lane_flag                  35040 non-null  float64       
 47  outer_shoulder_flag              35040 non-null  float64       
 48  ramp_flag                        35040 non-null  float64       
 49  overturn_accident_flag           35040 non-null  float64       
 50  construction_accident_flag       35040 non-null  float64       
 51  hazardous_material_vehicle_flag  35040 non-null  float64       
 52  on_fire_vehicle_flag             35040 non-null  float64       
 53  smoking_vehicle_flag             35040 non-null  float64       
 54  mainlane_disruption_flag         35040 non-null  float64       
 55  accident_vehicle_count           35040 non-null  float64       
 56  light_truck_count                35040 non-null  float64       
 57  passenger_car_count              35040 non-null  float64       
 58  bus_count                        35040 non-null  float64       
 59  heavy_truck_count                35040 non-null  float64       
 60  road_build                       35040 non-null  int64         
 61  total_block_count                35040 non-null  int64         
 62  road_block_count                 35040 non-null  int64         
dtypes: datetime64[ns](1), float64(31), int64(25), object(6)
memory usage: 17.1+ MB

Make sure to save the data in Parquet format — this will speed up data loading in the later stages before running model cross-validation.

# fast save, next time no need to wait for long processing
for df_name, df in results.items():
    object_columns = df.select_dtypes(include='object').columns
    df[object_columns] = df[object_columns].astype('string')
    df.to_parquet(f'../data/features/all_features_by_intergrantry/{df_name}.parquet')

Feature Importance | Feature Selection

The feature importance calculation method used in this study is particularly interesting. By applying TimeSeriesSplit, the dataset is incrementally divided into multiple time-based folds. Each fold serves as the basis for feature importance computation, effectively simulating a scenario of cumulative time progression. This design helps capture the influence of features that emerge over long-term temporal changes. Feature importance scores from each fold are aggregated and averaged, resulting in a stable estimate that reflects long-term patterns.

The core model used is Random Forest. As an ensemble of multiple decision trees, Random Forest remains robust even under high data variability, and its resulting feature importance scores are generally reliable.

In each loop, data is filtered according to different gantry pairs, and the rolling_window_feature_selection function is called with the model and data to perform feature selection. The results are then saved into the results_fs dictionary for later use.

def rolling_window_feature_selection(X, y, model, n_splits=5):
    tscv = TimeSeriesSplit(n_splits=n_splits)
    feature_importances = np.zeros(X.shape[1])

    for train_index, test_index in tscv.split(X):
        X_train, X_test = X.iloc[train_index], X.iloc[test_index]
        y_train, y_test = y.iloc[train_index], y.iloc[test_index]

        model.fit(X_train, y_train)
        y_pred = model.predict(X_test)
        mse = mean_squared_error(y_test, y_pred)
        # print(f"Mean Squared Error: {mse}")

        # accumulate feature importance
        feature_importances += model.feature_importances_

    # calculate average feature importance
    feature_importances /= n_splits
    return feature_importances

# store feature selection's results
results_fs = dict()

for item in tqdm(results.items()):
    cur_df_name = item[0]
    cur_df = item[1]

    if cur_df_name in results_fs.keys():
        continue

    # separate features and target variables
    X = cur_df.drop(columns=['WeightedAvgTravelTime', 'gf_gt', 'GantryFrom', 'GantryTo', 'TimeStamp', 'TotalTraffic'])
    y = cur_df['WeightedAvgTravelTime']

    # define base model
    base_model = RandomForestRegressor(n_estimators=100, random_state=42)

    # calculate feature importance
    feature_importances = rolling_window_feature_selection(X, y, base_model, n_splits=180)
    feature_importances_df = pd.DataFrame({'feature': X.columns,
                                           'importance': feature_importances
                                          }).sort_values(by='importance', ascending=False)

    # display most important feature
    selected_features = feature_importances_df['feature'].head(10).values
    print(f"df = {cur_df_name} Selected features: {selected_features}")

    results_fs[cur_df_name] = feature_importances_df.copy()

print('feature selection complete')

Lastly, we take a quick look at the feature importance distribution for a few selected gantry pairs. Here, we present only three plots as reference.

for i in results_fs.keys():
    print(i)
    display(results_fs[i].query('importance > 0'))

image by author

image by author

In the following experiments, I will use 0.00005 as the threshold for feature selection. This value is based on practical experience. While it is theoretically possible to retain all features for training, doing so would significantly increase training time without necessarily improving model performance. Setting a reasonable threshold helps preserve sufficient information while keeping the model efficient.

For the pair 05F0001N-03F0150N, the day of the week shows the strongest impact on travel time, followed by the length of consecutive holidays, and then effects from specific holidays. For 05F0287N-05F0055N, the most significant factor is the travel time from downstream segments, followed by day-of-week and some minor congestion-related features. In contrast, 05F0438N-05FR143N appears more influenced by road construction and day of the week.

In summary, feature importance varies significantly across different road segments. By using feature importance as a basis for selection, we can ensure that the resulting models are more aligned with the specific travel time characteristics of each target segment.

(Remember to save all feature importance results for use in the upcoming multivariate modeling phase.)

To improve the efficiency of upcoming multivariate model development, we brought back the features we carefully constructed earlier and used a Random Forest model — an embedded method — to evaluate feature importance. Now, we’re entering the part of this series that excites me the most, and also the final stage of the modeling process: Multivariate Modeling.

Note: This article was initially written in Chinese, then translated by ChatGPT and adjusted by myself. Slight differences in meaning may exist.

Travel Time Analysis Project Series

If you enjoyed this article, you can:

  • 👏 Clap a few times — no need to hit all 50
  • ✉️Follow me on Medium to stay updated and keep me motivated
  • 😜 Connect with me on LinkedIn — I’d love to exchange ideas

메타데이터
post_id
38ec38dfe89e
slug
travel-time-analysis-project-series-08-modeling-feature-selection-38ec38dfe89e
url
https://medium.com/@wh49hng/travel-time-analysis-project-series-08-modeling-feature-selection-38ec38dfe89e
canonical_url
https://medium.com/@wh49hng/travel-time-analysis-project-series-08-modeling-feature-selection-38ec38dfe89e
author_url
https://medium.com/@wh49hng
status
ok
fetched_at
2026-07-13 12:51:24