DoorDash · Data Science

Delivery Time Prediction

The work

  1. !pip install optuna
    Requirement already satisfied: optuna in /usr/local/lib/python3.13/dist-packages (5.0.0)
    Requirement already satisfied: alembic>=1.5.0 in /usr/local/lib/python3.13/dist-packages (from optuna) (1.20.0)
    Requirement already satisfied: colorlog in /usr/local/lib/python3.13/dist-packages (from optuna) (6.12.0)
    Requirement already satisfied: numpy in /usr/local/lib/python3.13/dist-packages (from optuna) (2.1.3)
    Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.13/dist-packages (from optuna) (26.3)
    Requirement already satisfied: sqlalchemy>=1.4.2 in /usr/local/lib/python3.13/dist-packages (from optuna) (2.0.52)
    Requirement already satisfied: tqdm in /usr/local/lib/python3.13/dist-packages (from optuna) (4.67.3)
    Requirement already satisfied: PyYAML in /usr/local/lib/python3.13/dist-packages (from optuna) (6.0.3)
    Requirement already satisfied: Mako in /usr/lib/python3/dist-packages (from alembic>=1.5.0->optuna) (1.3.2.dev0)
    Requirement already satisfied: typing-extensions>=4.12 in /usr/local/lib/python3.13/dist-packages (from alembic>=1.5.0->optuna) (4.16.0)
    Requirement already satisfied: greenlet>=1 in /usr/local/lib/python3.13/dist-packages (from sqlalchemy>=1.4.2->optuna) (3.5.5)
    
  2. import numpy as np
    import pandas as pd
    import seaborn as sns
    import matplotlib.pyplot as plt
    from scipy.stats import skew
    import joblib
    
    import lightgbm as lgb
    import optuna
    from sklearn.linear_model import Ridge
    from sklearn.metrics import root_mean_squared_error  # modern scikit-learn
    from sklearn.preprocessing import OneHotEncoder, PowerTransformer, StandardScaler
    import xgboost as xgb
    
    
    from sklearn.model_selection import train_test_split
    from sklearn.linear_model import LogisticRegression
    from sklearn.ensemble import RandomForestRegressor
    from sklearn.metrics import accuracy_score,confusion_matrix,mean_squared_error, r2_score,mean_absolute_error
    from sklearn.model_selection import KFold
    from sklearn.preprocessing import TargetEncoder
    
    
    from sklearn.pipeline import Pipeline
    from sklearn.impute import SimpleImputer
    from sklearn.compose import ColumnTransformer,TransformedTargetRegressor
    import zipfile
    import warnings
    warnings.filterwarnings('ignore')
  3. 
    with zipfile.ZipFile('/content/delivery-time-prediction-datasets.zip', 'r') as zip_ref:
        zip_ref.extractall('data')
  4. df_test=pd.read_csv('/content/data/delivery-time-prediction/deliveries_test.csv')
    df_train=pd.read_csv('/content/data/delivery-time-prediction/deliveries_train.csv')
  5. df_train.head()
       order_id      order_placed_at  restaurant_id  cuisine  \
    0    700001  2025-03-01 10:00:00             10    pizza   
    1    700003  2025-03-01 11:06:00              5  mexican   
    2    700004  2025-03-01 11:15:00             59    pizza   
    3    700005  2025-03-01 11:28:00             51   indian   
    4    700006  2025-03-01 11:40:00            104     thai   
    
       restaurant_avg_prep_minutes      city_zone  distance_km  items_count  \
    0                         12.9  suburbs_north          2.2            2   
    1                         12.4         uptown          5.3            1   
    2                         11.7  suburbs_south          2.6            3   
    3                         17.0       downtown          3.3            1   
    4                         11.5  suburbs_south         11.6            2   
    
       order_subtotal courier_vehicle  courier_trips_completed weather  \
    0           19.24         scooter                     50.0  cloudy   
    1            8.36            bike                   1255.0   clear   
    2           27.38         scooter                    276.0   clear   
    3            8.00            bike                    664.0  cloudy   
    4           18.96             car                     38.0    rain   
    
       delivery_minutes  
    0                29  
    1                36  
    2                31  
    3                34  
    4                54  
  6. df_train.shape
    (8000, 13)
  7. df_train.isnull().sum()
    order_id                         0
    order_placed_at                  0
    restaurant_id                    0
    cuisine                          0
    restaurant_avg_prep_minutes      0
    city_zone                        0
    distance_km                      0
    items_count                      0
    order_subtotal                   0
    courier_vehicle                  0
    courier_trips_completed        329
    weather                        228
    delivery_minutes                 0
    dtype: int64
  8. df_test.head(100)
        order_id      order_placed_at  restaurant_id  cuisine  \
    0     700002  2025-03-01 11:04:00             15   indian   
    1     700007  2025-03-01 11:52:00             37    pizza   
    2     700013  2025-03-01 12:30:00              3    pizza   
    3     700029  2025-03-01 16:15:00              4    pizza   
    4     700030  2025-03-01 16:17:00            145    pizza   
    ..       ...                  ...            ...      ...   
    95    700503  2025-03-10 12:04:00              5  mexican   
    96    700507  2025-03-10 12:41:00            160   salads   
    97    700509  2025-03-10 13:11:00             63    pizza   
    98    700510  2025-03-10 13:13:00            141     thai   
    99    700512  2025-03-10 13:54:00            127   indian   
    
        restaurant_avg_prep_minutes      city_zone  distance_km  items_count  \
    0                          18.1        midtown          1.4            2   
    1                          17.5        midtown          1.8            3   
    2                          20.9  suburbs_north          4.3            2   
    3                          14.0  suburbs_south          3.2            2   
    4                          12.3  suburbs_south          4.4            2   
    ..                          ...            ...          ...          ...   
    95                         12.4         uptown          6.8            2   
    96                          5.0         uptown          4.1            1   
    97                         12.6       downtown          3.2            4   
    98                         14.1  suburbs_north          5.4            2   
    99                         15.6       downtown          0.8            1   
    
        order_subtotal courier_vehicle  courier_trips_completed     weather  
    0            22.03             car                    181.0        rain  
    1            45.44             car                    501.0      cloudy  
    2            16.97            bike                    235.0        rain  
    3            15.96         scooter                     28.
  9. df_test.shape
    (2000, 12)
  10. df_test.isnull().sum()
    order_id                        0
    order_placed_at                 0
    restaurant_id                   0
    cuisine                         0
    restaurant_avg_prep_minutes     0
    city_zone                       0
    distance_km                     0
    items_count                     0
    order_subtotal                  0
    courier_vehicle                 0
    courier_trips_completed        75
    weather                        51
    dtype: int64
  11. EDA

  12. df_train['courier_trips_completed'].fillna(df_train['courier_trips_completed'].mean(),inplace=True)
  13. df_train.dropna(inplace=True)
  14. df_train.isnull().sum()
    order_id                       0
    order_placed_at                0
    restaurant_id                  0
    cuisine                        0
    restaurant_avg_prep_minutes    0
    city_zone                      0
    distance_km                    0
    items_count                    0
    order_subtotal                 0
    courier_vehicle                0
    courier_trips_completed        0
    weather                        0
    delivery_minutes               0
    dtype: int64
  15. df_train.dtypes
    order_id                         int64
    order_placed_at                 object
    restaurant_id                    int64
    cuisine                         object
    restaurant_avg_prep_minutes    float64
    city_zone                       object
    distance_km                    float64
    items_count                      int64
    order_subtotal                 float64
    courier_vehicle                 object
    courier_trips_completed        float64
    weather                         object
    delivery_minutes                 int64
    dtype: object
  16. print(df_train["restaurant_id"].nunique(), "restaurants in", len(df_train), "rows")
    print(df_train["restaurant_id"].value_counts().describe())
    160 restaurants in 7772 rows
    count    160.00000
    mean      48.57500
    std        6.79951
    min       31.00000
    25%       43.00000
    50%       48.00000
    75%       54.00000
    max       63.00000
    Name: count, dtype: float64
    
  17. rest_mean = df_train.groupby("restaurant_id")["delivery_minutes"].mean()
    print(rest_mean.describe())
    print("Std of restaurant means:", rest_mean.std())
    count    160.000000
    mean      32.018862
    std        6.571982
    min       17.595745
    25%       27.187867
    50%       32.200130
    75%       36.098214
    max       50.952381
    Name: delivery_minutes, dtype: float64
    Std of restaurant means: 6.571982417268051
    
  18. test_rest = set(df_test["restaurant_id"])
    train_rest = set(df_train["restaurant_id"])
    print("Share of test restaurants seen in train:", len(test_rest & train_rest) / len(test_rest))
    Share of test restaurants seen in train: 1.0
    
  19. df_train.drop(columns=['order_id'],inplace=True)
  20. df_train.head()
           order_placed_at  restaurant_id  cuisine  restaurant_avg_prep_minutes  \
    0  2025-03-01 10:00:00             10    pizza                         12.9   
    1  2025-03-01 11:06:00              5  mexican                         12.4   
    2  2025-03-01 11:15:00             59    pizza                         11.7   
    3  2025-03-01 11:28:00             51   indian                         17.0   
    4  2025-03-01 11:40:00            104     thai                         11.5   
    
           city_zone  distance_km  items_count  order_subtotal courier_vehicle  \
    0  suburbs_north          2.2            2           19.24         scooter   
    1         uptown          5.3            1            8.36            bike   
    2  suburbs_south          2.6            3           27.38         scooter   
    3       downtown          3.3            1            8.00            bike   
    4  suburbs_south         11.6            2           18.96             car   
    
       courier_trips_completed weather  delivery_minutes  
    0                     50.0  cloudy                29  
    1                   1255.0   clear                36  
    2                    276.0   clear                31  
    3                    664.0  cloudy                34  
    4                     38.0    rain                54  
  21. df_train
              order_placed_at  restaurant_id  cuisine  \
    0     2025-03-01 10:00:00             10    pizza   
    1     2025-03-01 11:06:00              5  mexican   
    2     2025-03-01 11:15:00             59    pizza   
    3     2025-03-01 11:28:00             51   indian   
    4     2025-03-01 11:40:00            104     thai   
    ...                   ...            ...      ...   
    7995  2025-08-31 21:22:00            126    pizza   
    7996  2025-08-31 22:12:00             51   indian   
    7997  2025-08-31 23:05:00            133  chinese   
    7998  2025-08-31 23:39:00             89    sushi   
    7999  2025-08-31 23:42:00            138     thai   
    
          restaurant_avg_prep_minutes      city_zone  distance_km  items_count  \
    0                            12.9  suburbs_north          2.2            2   
    1                            12.4         uptown          5.3            1   
    2                            11.7  suburbs_south          2.6            3   
    3                            17.0       downtown          3.3            1   
    4                            11.5  suburbs_south         11.6            2   
    ...                           ...            ...          ...          ...   
    7995                         12.0        midtown          1.6            3   
    7996                         17.0       downtown          3.8            1   
    7997                         15.9  suburbs_north          5.7            3   
    7998                         22.3  suburbs_north          7.6            1   
    7999                         16.2         uptown          1.8            6   
    
          order_subtotal courier_vehicle  courier_trips_completed     weather  \
    0              19.24         scooter                     50.0      cloudy   
    1               8.36            bike                   1255.0       clear   
    2              27.38         scooter                    276.0       clear   
    3               8.00            bike                    664.0      cloudy   
    4              18.96             car     
  22. # count plot for category columns
    plt.figure(figsize=(10,10))
    sns.countplot(x='cuisine',data=df_train,palette='Set2')
    Chart produced by this cell
    <Axes: xlabel='cuisine', ylabel='count'>
    <Figure size 1000x1000 with 1 Axes>
  23. # count plot for courier_vehicle
    plt.figure(figsize=(10,10))
    sns.countplot(x='courier_vehicle',data=df_train,palette='Set2')
    Chart produced by this cell
    <Axes: xlabel='courier_vehicle', ylabel='count'>
    <Figure size 1000x1000 with 1 Axes>
  24. # count plot for city_zone
    plt.figure(figsize=(10,10))
    sns.countplot(x='city_zone',data=df_train,palette='Set2')
    Chart produced by this cell
    <Axes: xlabel='city_zone', ylabel='count'>
    <Figure size 1000x1000 with 1 Axes>
  25. df_train.describe()
           restaurant_id  restaurant_avg_prep_minutes  distance_km  items_count  \
    count    7772.000000                  7772.000000  7772.000000  7772.000000   
    mean       81.041688                    13.851595     3.581009     2.797735   
    std        46.438977                     4.066137     2.329842     1.655508   
    min         1.000000                     5.000000     0.600000     1.000000   
    25%        41.000000                    11.500000     2.000000     2.000000   
    50%        81.000000                    13.400000     2.900000     2.000000   
    75%       121.000000                    16.800000     4.400000     4.000000   
    max       160.000000                    22.300000    13.300000     8.000000   
    
           order_subtotal  courier_trips_completed  delivery_minutes  
    count     7772.000000              7772.000000       7772.000000  
    mean        32.223913               354.878442         32.056613  
    std         20.927832               898.077507         11.145954  
    min          8.000000                 1.000000          8.000000  
    25%         15.740000                64.000000         25.000000  
    50%         27.395000               158.000000         31.000000  
    75%         42.537500               355.163734         37.000000  
    max        127.640000             45151.000000        139.000000  
  26. plt.figure(figsize=(10,10))
    sns.boxplot(x='cuisine',y='delivery_minutes',data=df_train,palette='Set2')
    plt.title('Distribution of Target Value by Category', fontsize=14, fontweight='bold')
    plt.xlabel('String Category Columns', fontsize=12)
    plt.ylabel('Target Column:Delivery Time', fontsize=12)
    
    # 5. Display the plot
    plt.show()
    Chart produced by this cell
    <Figure size 1000x1000 with 1 Axes>
  27. # dot plot
    plt.figure(figsize=(10,10))
    sns.stripplot(x='courier_vehicle',y='delivery_minutes',data=df_train,palette='Set2')
    Chart produced by this cell
    <Axes: xlabel='courier_vehicle', ylabel='delivery_minutes'>
    <Figure size 1000x1000 with 1 Axes>
  28. # 1. Check Skewness for Numerical Columns
    numerical_skew = df_train.select_dtypes(include=['number']).skew()
    print("--- Skewness of Numerical Features & Target ---")
    print(numerical_skew)
    --- Skewness of Numerical Features & Target ---
    restaurant_id                  -0.022378
    restaurant_avg_prep_minutes    -0.041040
    distance_km                     1.473835
    items_count                     1.115513
    order_subtotal                  1.398112
    courier_trips_completed        22.526327
    delivery_minutes                2.612094
    dtype: float64
    
  29. target_col='delivery_minutes'
    
    plt.figure(figsize=(10,10))
    correlation=df_train.corr(numeric_only=True)['delivery_minutes']
    sns.heatmap(df_train.corr(numeric_only=True),annot=True)
    Chart produced by this cell
    <Axes: >
    <Figure size 1000x1000 with 2 Axes>
  30. df_train['cuisine'].value_counts()
    cuisine
    thai       1457
    pizza      1296
    indian     1174
    salads      921
    mexican     916
    chinese     776
    sushi       757
    burgers     475
    Name: count, dtype: int64
  31. df_train.groupby('cuisine')['delivery_minutes'].agg(['mean', 'std', 'count']).sort_values('mean')
                  mean        std  count
    cuisine                             
    salads   24.984799   9.219650    921
    mexican  28.496725  10.350628    916
    burgers  28.907368  11.279151    475
    chinese  32.046392  10.840684    776
    thai     32.750858  10.506034   1457
    pizza    32.813272  10.111686   1296
    sushi    35.355350   9.541985    757
    indian   37.839012  11.843415   1174
  32. df_train['hour']=pd.to_datetime(df_train['order_placed_at']).dt.hour
  33. print((df_train['delivery_minutes'] > 80).sum())
    print((df_train['delivery_minutes'] > 100).sum())
    df_train.nlargest(20, 'delivery_minutes')[['delivery_minutes', 'cuisine', 'weather', 'hour', 'distance_km']]
    42
    29
    
          delivery_minutes  cuisine     weather  hour  distance_km
    4107               139     thai       clear    20          3.7
    1934               137  mexican  heavy_rain    12          2.6
    5677               137   indian      cloudy    12          3.5
    4904               136     thai       clear    13          0.9
    6391               136   indian      cloudy    18          2.3
    976                135  mexican        rain    20          2.3
    5416               135   indian        rain    21          1.2
    2637               133  chinese       clear    17          5.4
    1238               131     thai       clear    12          8.3
    5042               131   indian       clear    19          3.5
    3793               130   indian       clear    18          1.9
    858                129   indian  heavy_rain    17          2.6
    3197               128  chinese      cloudy    20          4.3
    389                127    pizza       clear    19          2.5
    2311               126     thai       clear    17          6.8
    700                121  mexican       clear    19          3.1
    1642               120     thai        rain    21          1.3
    3689               120   indian       clear    11          4.1
    2255               117  mexican       clear    13         10.6
    2038               115    sushi       clear    18          1.4
  34. Feature Enginnering

  35. df_train
              order_placed_at  restaurant_id  cuisine  \
    0     2025-03-01 10:00:00             10    pizza   
    1     2025-03-01 11:06:00              5  mexican   
    2     2025-03-01 11:15:00             59    pizza   
    3     2025-03-01 11:28:00             51   indian   
    4     2025-03-01 11:40:00            104     thai   
    ...                   ...            ...      ...   
    7995  2025-08-31 21:22:00            126    pizza   
    7996  2025-08-31 22:12:00             51   indian   
    7997  2025-08-31 23:05:00            133  chinese   
    7998  2025-08-31 23:39:00             89    sushi   
    7999  2025-08-31 23:42:00            138     thai   
    
          restaurant_avg_prep_minutes      city_zone  distance_km  items_count  \
    0                            12.9  suburbs_north          2.2            2   
    1                            12.4         uptown          5.3            1   
    2                            11.7  suburbs_south          2.6            3   
    3                            17.0       downtown          3.3            1   
    4                            11.5  suburbs_south         11.6            2   
    ...                           ...            ...          ...          ...   
    7995                         12.0        midtown          1.6            3   
    7996                         17.0       downtown          3.8            1   
    7997                         15.9  suburbs_north          5.7            3   
    7998                         22.3  suburbs_north          7.6            1   
    7999                         16.2         uptown          1.8            6   
    
          order_subtotal courier_vehicle  courier_trips_completed     weather  \
    0              19.24         scooter                     50.0      cloudy   
    1               8.36            bike                   1255.0       clear   
    2              27.38         scooter                    276.0       clear   
    3               8.00            bike                    664.0      cloudy   
    4              18.96             car     
  36. 
    df_train['order_placed_at'] = pd.to_datetime(df_train['order_placed_at'])
    
    # 3. Extract the hour component
    df_train['hour'] = df_train['order_placed_at'].dt.hour
    
    # 4. Count orders per hour
    hourly_counts = df_train.groupby('hour').size().reset_index(name='order_count')
    
    # 5. Identify highest and lowest order times
    peak_hour = hourly_counts.loc[hourly_counts['order_count'].idxmax()]
    low_hour = hourly_counts.loc[hourly_counts['order_count'].idxmin()]
    
    print(f"Highest order volume is at {peak_hour['hour']}:00 with {peak_hour['order_count']} orders.")
    print(f"Lowest order volume is at {low_hour['hour']}:00 with {low_hour['order_count']} orders.")
    
    # 6. Plot the visualization
    plt.figure(figsize=(10, 5))
    plt.plot(hourly_counts['hour'], hourly_counts['order_count'], marker='o', color='royalblue', linewidth=2)
    
    # Highlight high and low points
    plt.scatter(peak_hour['hour'], peak_hour['order_count'], color='red', s=100, zorder=5, label='Highest Volume')
    plt.scatter(low_hour['hour'], low_hour['order_count'], color='darkorange', s=100, zorder=5, label='Lowest Volume')
    
    # Formatting the chart
    plt.title('Order Volume Trends by Hour of Day', fontsize=14, fontweight='bold')
    plt.xlabel('Hour of Day (0 - 23)', fontsize=12)
    plt.ylabel('Total Orders Placed', fontsize=12)
    plt.xticks(range(0, 24))
    plt.grid(axis='both', linestyle='--', alpha=0.7)
    plt.legend()
    
    # Display the chart
    plt.tight_layout()
    plt.show()
    Highest order volume is at 19:00 with 1040 orders.
    Lowest order volume is at 23:00 with 142 orders.
    
    Chart produced by this cell
    <Figure size 1000x500 with 1 Axes>
  37. df_train['order_placed_at']=pd.to_datetime(df_train['order_placed_at'])
  38. df_train['day_of_week'] = (
        df_train['order_placed_at'].dt.dayofweek
    )
  39. weekday_hour = (
        df_train
        .groupby(['day_of_week', 'hour'])
        .size()
        .reset_index(name='order_count')
    )
    
    heatmap_data = weekday_hour.pivot(
        index='day_of_week',
        columns='hour',
        values='order_count'
    ).fillna(0)
  40. # heat map of weekday hour data
    plt.imshow(
        heatmap_data,
        aspect='auto',
        interpolation='nearest'
    )
    
    plt.colorbar(label='Number of Orders')
    
    plt.xticks(
        range(len(heatmap_data.columns)),
        heatmap_data.columns
    )
    
    plt.yticks(
        range(7),
        ['Monday', 'Tuesday', 'Wednesday',
         'Thursday', 'Friday', 'Saturday', 'Sunday']
    )
    
    plt.xlabel('Hour of Day')
    plt.ylabel('Weekday')
    plt.title('Order Volume: Weekday vs Hour')
    
    plt.tight_layout()
    plt.show()
    Chart produced by this cell
    <Figure size 640x480 with 2 Axes>
  41. # drop the column order_placed_at as it doesnot have any relation with the output columns
    df_train.drop(columns=['order_placed_at'],inplace=True)
  42. # creating a new features for order_total_prep_time
    df_train['order_total_prep_time']=df_train['restaurant_avg_prep_minutes']*df_train['items_count']
  43. weather_impact_map = {
        "clear": 0,
        "cloudy": 1,
        "misty": 2,
        "rain": 3,
        "heavy_rain": 5
    }
    df_train['weather_impact_score'] = df_train['weather'].map(weather_impact_map)
  44. df_train['hour_sin'] = np.sin(2 * np.pi * df_train['hour'] / 24)
    df_train['hour_cos'] = np.cos(2 * np.pi * df_train['hour'] / 24)
  45. cuisine_hour = df_train.groupby(['cuisine', 'hour'])['delivery_minutes'].mean().unstack()
    print(cuisine_hour)
    hour            10         11         12         13         14         15  \
    cuisine                                                                     
    burgers  23.214286  31.650000  28.829787  32.836364  30.125000  30.846154   
    chinese  29.533333  32.451613  32.712766  33.362500  28.615385  28.923077   
    indian   33.774194  39.010989  39.044444  39.209091  33.750000  33.071429   
    mexican  25.375000  29.323944  29.879630  31.959459  26.741935  24.888889   
    pizza    28.684211  34.494505  33.302469  34.134921  29.281250  32.361111   
    salads   20.448276  26.705128  24.207207  27.099010  23.040000  24.000000   
    sushi    31.333333  37.550000  37.500000  36.538462  33.463415  33.461538   
    thai     31.666667  34.064220  33.864407  34.040000  27.970149  28.897436   
    
    hour            16         17         18         19         20         21  \
    cuisine                                                                     
    burgers  25.454545  28.191489  28.802817  29.650000  27.888889  28.090909   
    chinese  29.969697  31.096774  34.200000  33.691057  32.868852  29.210526   
    indian   33.938776  34.710000  40.719745  39.032895  41.106796  36.354430   
    mexican  24.047619  26.541667  29.833333  29.162791  30.056180  28.584906   
    pizza    29.684211  31.297030  34.122807  34.742690  33.218750  31.263158   
    salads   20.800000  22.968750  26.821429  26.409836  25.636364  22.246154   
    sushi    32.500000  31.866667  37.845455  35.494845  35.893939  33.404255   
    thai     33.388060  31.446281  33.913793  33.500000  33.694444  30.747475   
    
    hour            22         23  
    cuisine                        
    burgers  21.739130  29.444444  
    chinese  29.263158  28.000000  
    indian   33.784314  36.772727  
    mexican  24.928571  24.545455  
    pizza    30.245283  31.909091  
    salads   23.093023  26.000000  
    sushi    33.285714  36.375000  
    thai     31.290323  28.652174  
    
  46. plt.figure(figsize=(14, 6))
    sns.heatmap(cuisine_hour, cmap='YlOrRd', annot=False, cbar_kws={'label': 'Avg delivery minutes'})
    plt.title('Delivery Time: Cuisine × Hour')
    plt.xlabel('Hour')
    plt.ylabel('Cuisine')
    plt.tight_layout()
    plt.show()
    Chart produced by this cell
    <Figure size 1400x600 with 2 Axes>
  47. correlation = df_train['hour_cos'].corr(df_train['delivery_minutes'])
    print(correlation)
    -0.02187353289435526
    
  48. # new feature for peak hour as it effect the delivery time
    df_train['is_dinner_peak'] = df_train['hour'].isin([18, 19, 20]).astype(int)
  49. # seperate the columns according to the features data types
    num_cols = df_train.select_dtypes(include=["int64", "float64"]).columns.tolist()
    cat_cols = df_train.select_dtypes(include=["object", "category"]).columns.tolist()
    print(num_cols)
    print(cat_cols)
    ['restaurant_id', 'restaurant_avg_prep_minutes', 'distance_km', 'items_count', 'order_subtotal', 'courier_trips_completed', 'delivery_minutes', 'order_total_prep_time', 'weather_impact_score', 'hour_sin', 'hour_cos', 'is_dinner_peak']
    ['cuisine', 'city_zone', 'courier_vehicle', 'weather']
    
  50. # drop the column as it doesnot have any effect or corelation on target features
    df_train.drop(columns=['hour'],inplace=True)
  51. df_train.drop(columns=['day_of_week'],inplace=True)
  52. Peprocessing and training

  53. df_train
          restaurant_id  cuisine  restaurant_avg_prep_minutes      city_zone  \
    0                10    pizza                         12.9  suburbs_north   
    1                 5  mexican                         12.4         uptown   
    2                59    pizza                         11.7  suburbs_south   
    3                51   indian                         17.0       downtown   
    4               104     thai                         11.5  suburbs_south   
    ...             ...      ...                          ...            ...   
    7995            126    pizza                         12.0        midtown   
    7996             51   indian                         17.0       downtown   
    7997            133  chinese                         15.9  suburbs_north   
    7998             89    sushi                         22.3  suburbs_north   
    7999            138     thai                         16.2         uptown   
    
          distance_km  items_count  order_subtotal courier_vehicle  \
    0             2.2            2           19.24         scooter   
    1             5.3            1            8.36            bike   
    2             2.6            3           27.38         scooter   
    3             3.3            1            8.00            bike   
    4            11.6            2           18.96             car   
    ...           ...          ...             ...             ...   
    7995          1.6            3           28.80             car   
    7996          3.8            1           15.74            bike   
    7997          5.7            3           21.27             car   
    7998          7.6            1           13.78             car   
    7999          1.8            6           93.31         scooter   
    
          courier_trips_completed     weather  delivery_minutes  \
    0                        50.0      cloudy                29   
    1                      1255.0       clear                36   
    2                       276.0       clear                31   
    3                       664.0      cloudy 
  54. upper_cap = df_train["delivery_minutes"].quantile(0.99)
    
    X = df_train.drop(columns=["delivery_minutes", "order_id", "hour", "day_of_week"],
                      errors="ignore").copy()
    X["restaurant_id"] = X["restaurant_id"].astype(str)
    y = df_train["delivery_minutes"].clip(upper=upper_cap).astype(float)
    
    rest_cols   = ["restaurant_id"]
    binary_cols = ["is_dinner_peak"]
    exclude     = set(rest_cols + binary_cols)
    
    cat_cols = [c for c in X.select_dtypes(include=["object", "category"]).columns
                if c not in exclude]
    num_cols = [c for c in X.select_dtypes(include="number").columns
                if c not in exclude]
    
    skew = X[num_cols].skew()
    skewed_num_cols = skew[skew > 1].index.tolist()
    normal_num_cols = [c for c in num_cols if c not in skewed_num_cols]
    
    print("skewed:", skewed_num_cols)
    print("normal:", normal_num_cols)
    print("cat:   ", cat_cols)
    print("binary:", binary_cols)
    print("rest:  ", rest_cols)
    
    all_lists = skewed_num_cols + normal_num_cols + cat_cols + binary_cols + rest_cols
    assert len(all_lists) == len(set(all_lists)), "a column is in two lists"
    skewed: ['distance_km', 'items_count', 'order_subtotal', 'courier_trips_completed', 'order_total_prep_time', 'weather_impact_score']
    normal: ['restaurant_avg_prep_minutes', 'hour_sin', 'hour_cos']
    cat:    ['cuisine', 'city_zone', 'courier_vehicle', 'weather']
    binary: ['is_dinner_peak']
    rest:   ['restaurant_id']
    
  55. # X = df_train.drop(columns=["delivery_minutes", "order_id"], errors="ignore").copy()
    # X["restaurant_id"] = X["restaurant_id"].astype(str)
    # y = df_train["delivery_minutes"].clip(upper=upper_cap).astype(float)
  56. # X_train, X_val, y_train, y_val = train_test_split(
    #     X, y, test_size=0.2, random_state=42
    # )
  57. # # 2. Get Column Lists
    # rest_cols = ["restaurant_id"]
    # binary_cols = ["is_dinner_peak"]
    # exclude = set(rest_cols + binary_cols)
    
    # cat_cols = [c for c in X.select_dtypes(include=["object", "category"]).columns
    #             if c not in exclude]
    # num_cols = [c for c in X.select_dtypes(include="number").columns
    #             if c not in exclude]
    
    # # 3. Categorize numerical columns based on skewness
    # skewed_num_cols = numerical_skew[numerical_skew > 1].index.tolist()
    # normal_num_cols = numerical_skew[numerical_skew <= 1].index.tolist()
    
    # if "delivery_minutes" in skewed_num_cols:
    #     skewed_num_cols.remove("delivery_minutes")
    # if "delivery_minutes" in normal_num_cols:
    #     normal_num_cols.remove("delivery_minutes")
  58. print("skewed:", skewed_num_cols)
    print("normal:", normal_num_cols)
    print("cat:   ", cat_cols)
    print("binary:", binary_cols)
    print("rest:  ", rest_cols)
    skewed: ['distance_km', 'items_count', 'order_subtotal', 'courier_trips_completed', 'order_total_prep_time', 'weather_impact_score']
    normal: ['restaurant_avg_prep_minutes', 'hour_sin', 'hour_cos']
    cat:    ['cuisine', 'city_zone', 'courier_vehicle', 'weather']
    binary: ['is_dinner_peak']
    rest:   ['restaurant_id']
    
  59. 
    def buildPipeline(trial):
        # --- STEP A: PREPROCESSING ---
        use_skew_fix = trial.suggest_categorical("use_skew_fix", [True, False])
    
        if use_skew_fix:
            skewed_transformer = Pipeline(
                steps=[
                    ("imputer", SimpleImputer(strategy="median")),
                    ("skew_fix", PowerTransformer(method="yeo-johnson")),
                    ("scaler", StandardScaler()),
                ]
            )
        else:
            skewed_transformer = Pipeline(
                steps=[
                    ("imputer", SimpleImputer(strategy="median")),
                    ("scaler", StandardScaler()),
                ]
            )
    
        normal_transformer = Pipeline(
            steps=[
                ("imputer", SimpleImputer(strategy="mean")),
                ("scaler", StandardScaler()),
            ]
        )
    
        cat_transformer = Pipeline(
            steps=[
                ("imputer", SimpleImputer(strategy="most_frequent")),
                ("encoder", OneHotEncoder(handle_unknown="ignore", sparse_output=False)),
            ]
        )
    
        binary_transformer = Pipeline(steps=[
            ("imputer", SimpleImputer(strategy="most_frequent")),
        ])
    
        rest_transformer = Pipeline(steps=[
        ("imputer", SimpleImputer(strategy="most_frequent")),
        ("te", TargetEncoder(target_type="continuous", smooth="auto", random_state=42)),
        ])
    
        preprocessor = ColumnTransformer(
            transformers=[
                ("skew_num", skewed_transformer, skewed_num_cols),
                ("norm_num", normal_transformer, normal_num_cols),
                ("cat", cat_transformer, cat_cols),
                ("binary", binary_transformer, binary_cols),
                ("rest", rest_transformer, rest_cols),
    
            ]
        )
    
        # --- STEP B: TUNE MODELS WITH TARGET TRANSFORMATION ---
        model_type = trial.suggest_categorical(
            "model_type", ["ridge", "xgboost", "lightgbm", "random_forest"]
        )
    
        if model_type == "ridge":
            base_model = Ridge(
                alpha=trial.suggest_float("ridge_alpha", 1e-3, 100.0, log=True)
            )
            model = TransformedTargetRegressor(
                regressor=base_model, func=np.log1p, inverse_func=np.expm1
            )
    
        elif model_type == "xgboost":
            base_model = xgb.XGBRegressor(
                n_estimators=trial.suggest_int("xgb_n_estimators", 50, 300),
                max_depth=trial.suggest_int("xgb_max_depth", 3, 9),
                learning_rate=trial.suggest_float("xgb_lr", 1e-3, 0.3, log=True),
            )
            model = TransformedTargetRegressor(
                regressor=base_model, func=np.log1p, inverse_func=np.expm1
            )
    
        elif model_type == "lightgbm":
            base_model = lgb.LGBMRegressor(
                n_estimators=trial.suggest_int("lgb_n_estimators", 50, 600),  # widened
                max_depth=trial.suggest_int("lgb_max_depth", 2, 9),           # widened lower bound
                learning_rate=trial.suggest_float("lgb_lr", 1e-3, 0.3, log=True),
                num_leaves=trial.suggest_int("lgb_num_leaves", 15, 127),      # new — controls complexity independent of depth
                subsample=trial.suggest_float("lgb_subsample", 0.6, 1.0),     # new — row sampling, helps generalization
                colsample_bytree=trial.suggest_float("lgb_colsample", 0.6, 1.0),  # new — feature sampling
                reg_alpha=trial.suggest_float("lgb_reg_alpha", 1e-8, 10.0, log=True),   # new — L1 regularization
                reg_lambda=trial.suggest_float("lgb_reg_lambda", 1e-8, 10.0, log=True), # new — L2 regularization
                verbose=-1,
            )
            model = TransformedTargetRegressor(
                regressor=base_model, func=np.log1p, inverse_func=np.expm1
            )
    
        elif model_type == "random_forest":
            base_model = RandomForestRegressor(
                n_estimators=trial.suggest_int("rf_n_estimators", 50, 300),
                max_depth=trial.suggest_int("rf_max_depth", 3, 20),
                min_samples_split=trial.suggest_int("rf_min_samples_split", 2, 10),
                min_samples_leaf=trial.suggest_int("rf_min_samples_leaf", 1, 10),
                max_features=trial.suggest_categorical("rf_max_features", ["sqrt", "log2", None]),
                n_jobs=-1,
                random_state=42,
            )
            use_log_target = trial.suggest_categorical("rf_use_log_target", [True, False])
            if use_log_target:
                model = TransformedTargetRegressor(
                    regressor=base_model, func=np.log1p, inverse_func=np.expm1
                )
            else:
                model = base_model
    
        # --- STEP C: CREATE FINAL PIPELINE ---
        full_pipeline = Pipeline(
            steps=[("preprocessor", preprocessor), ("regressor", model)]
        )
        return full_pipeline
  60. def objective(trial):
        full_pipeline = buildPipeline(trial)
        kf = KFold(n_splits=10, shuffle=True, random_state=42)
        fold_rmses = []
        for fold_idx, (tr, va) in enumerate(kf.split(X)):
            full_pipeline.fit(X.iloc[tr], y.iloc[tr])
            preds = full_pipeline.predict(X.iloc[va])
            fold_rmses.append(root_mean_squared_error(y.iloc[va], preds))
            trial.report(np.mean(fold_rmses), step=fold_idx)
            if trial.should_prune():
                raise optuna.TrialPruned()
        return np.mean(fold_rmses)
  61. # 3. Run the study
    study = optuna.create_study(direction="minimize")
    study.optimize(objective, n_trials=100)
    
    
    print("Best Parameters:", study.best_params)
    [I 2026-09-28 07:23:42,165] A new study created in memory with name: no-name-5135ed56-f2d7-404f-a405-85d72bbb7634
    [I 2026-09-28 07:23:44,576] Trial 0 finished with value: 4.876854451981479 and parameters: {'use_skew_fix': True, 'model_type': 'ridge', 'ridge_alpha': 4.356142972554358}. Best is trial 0 with value: 4.876854451981479.
    [I 2026-09-28 07:24:06,634] Trial 1 finished with value: 5.220018141216767 and parameters: {'use_skew_fix': True, 'model_type': 'random_forest', 'rf_n_estimators': 262, 'rf_max_depth': 12, 'rf_min_samples_split': 2, 'rf_min_samples_leaf': 1, 'rf_max_features': 'log2', 'rf_use_log_target': False}. Best is trial 0 with value: 4.876854451981479.
    [I 2026-09-28 07:24:08,517] Trial 2 finished with value: 5.041493553060039 and parameters: {'use_skew_fix': False, 'model_type': 'lightgbm', 'lgb_n_estimators': 178, 'lgb_max_depth': 4, 'lgb_lr': 0.015743572084116194, 'lgb_num_leaves': 57, 'lgb_subsample': 0.6796070219391592, 'lgb_colsample': 0.9188092666374903, 'lgb_reg_alpha': 7.073141373873041e-05, 'lgb_reg_lambda': 0.0010056692009576833}. Best is trial 0 with value: 4.876854451981479.
    [I 2026-09-28 07:24:09,640] Trial 3 finished with value: 4.886470953457954 and parameters: {'use_skew_fix': True, 'model_type': 'ridge', 'ridge_alpha': 38.432129098869865}. Best is trial 0 with value: 4.876854451981479.
    [I 2026-09-28 07:24:16,159] Trial 4 finished with value: 6.273408244976615 and parameters: {'use_skew_fix': False, 'model_type': 'random_forest', 'rf_n_estimators': 172, 'rf_max_depth': 5, 'rf_min_samples_split': 4, 'rf_min_samples_leaf': 1, 'rf_max_features': 'sqrt', 'rf_use_log_target': True}. Best is trial 0 with value: 4.876854451981479.
    [I 2026-09-28 07:24:17,014] Trial 5 pruned. 
    [I 2026-09-28 07:24:19,596] Trial 6 finished with value: 4.704178035608584 and parameters: {'use_skew_fix': False, 'model_type': 'xgboost', 'xgb_n_estimators': 126, 'xgb_max_depth': 6, 'xgb_lr': 0.2687035571477468}. Best is trial 6 with value: 4.704178035608584.
    [I 2026
  62. print(df_train['delivery_minutes'].describe())
    count    7772.000000
    mean       32.056613
    std        11.145954
    min         8.000000
    25%        25.000000
    50%        31.000000
    75%        37.000000
    max       139.000000
    Name: delivery_minutes, dtype: float64
    
  63. Evaluation

  64. 
    y_raw = df_train["delivery_minutes"]          # original, uncapped
    y_capped = y                                  # your capped target
    best_pipeline = buildPipeline(optuna.trial.FixedTrial(study.best_params))
    
    kf = KFold(n_splits=10, shuffle=True, random_state=42)
    rmse_capped, rmse_raw, mae_raw = [], [], []
    
    for tr, va in kf.split(X):
        best_pipeline.fit(X.iloc[tr], y_capped.iloc[tr])
        p = best_pipeline.predict(X.iloc[va])
        rmse_capped.append(root_mean_squared_error(y_capped.iloc[va], p))
        rmse_raw.append(root_mean_squared_error(y_raw.iloc[va], p))
        mae_raw.append(mean_absolute_error(y_raw.iloc[va], p))
    
    print(f"RMSE (capped target):   {np.mean(rmse_capped):.3f}")
    print(f"RMSE (original target): {np.mean(rmse_raw):.3f}")
    print(f"MAE  (original target): {np.mean(mae_raw):.3f}")
    RMSE (capped target):   4.411
    RMSE (original target): 7.122
    MAE  (original target): 3.254
    
  65. best_pipeline.fit(X, y_capped)
    names = best_pipeline.named_steps["preprocessor"].get_feature_names_out()
    print([n for n in names if "rest" in n])
    print(X["restaurant_id"].nunique(), X["restaurant_id"].dtype)
    ['norm_num__restaurant_avg_prep_minutes', 'rest__restaurant_id']
    160 object
    
  66. p = buildPipeline(optuna.trial.FixedTrial({
        "use_skew_fix": True, "model_type": "ridge", "ridge_alpha": 1.0}))
    p.fit(X, y)
    names = p.named_steps["preprocessor"].get_feature_names_out()
    print([n for n in names if "restaurant" in n or "dinner" in n])
    ['norm_num__restaurant_avg_prep_minutes', 'binary__is_dinner_peak', 'rest__restaurant_id']
    
  67. final_pipeline = buildPipeline(optuna.trial.FixedTrial(study.best_params))
    final_pipeline.fit(X, y_capped)
    joblib.dump(final_pipeline, "delivery_time_model.joblib")
    ['delivery_time_model.joblib']
  68. Predection

  69. def prepare(df):
        df = df.copy()
        df["order_placed_at"] = pd.to_datetime(df["order_placed_at"])
        df["hour"] = df["order_placed_at"].dt.hour
        df["hour_sin"] = np.sin(2 * np.pi * df["hour"] / 24)
        df["hour_cos"] = np.cos(2 * np.pi * df["hour"] / 24)
        df["is_dinner_peak"] = df["hour"].isin([18, 19, 20]).astype(int)
        df["weather_impact_score"] = df["weather"].map(weather_impact_map)
        df['order_total_prep_time']=df['restaurant_avg_prep_minutes']*df['items_count']
        df["restaurant_id"] = df["restaurant_id"].astype(str)
        return df
  70. X_test_full = prepare(df_test)
    print("Missing in test:", set(X.columns) - set(X_test_full.columns))
    X_test = X_test_full[X.columns]
    Missing in test: set()
    
  71. X_test_full = prepare(df_test)
    print("Missing in test:", set(X.columns) - set(X_test_full.columns))   # should print set()
    X_test = X_test_full[X.columns]
    print(X_test.isna().sum()[lambda s: s > 0])                           # should be empty
    test_preds = final_pipeline.predict(X_test)
    print(pd.Series(test_preds).describe())
    Missing in test: set()
    courier_trips_completed    75
    weather                    51
    weather_impact_score       51
    dtype: int64
    count    2000.000000
    mean       31.357023
    std         8.564023
    min        11.728745
    25%        25.258527
    50%        30.656190
    75%        36.551810
    max        67.449295
    dtype: float64
    
  72. submission = pd.DataFrame({
        "order_id": df_test["order_id"],
        "delivery_minutes": test_preds,
    })
    submission.to_csv("submission.csv", index=False)
    print(submission.shape, submission.head())
    (2000, 2)    order_id  delivery_minutes
    0    700002         28.465158
    1    700007         29.474197
    2    700013         51.899017
    3    700029         29.059341
    4    700030         30.943319
    

The brief this answers

Build and evaluate a model that predicts food delivery times, then explain what makes deliveries slow.

Try it yourself

Other answers to this brief

The same problem, solved by somebody else.

Discussion

Nothing here yet. Ask how they approached it, or tell them what you would have done differently.