DoorDash · Data Science
Delivery Time Prediction
The work
- In [1]:
!pip install optunaRequirement 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)
- In [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') - In [3]:
with zipfile.ZipFile('/content/delivery-time-prediction-datasets.zip', 'r') as zip_ref: zip_ref.extractall('data') - In [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') - In [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
- In [6]:
df_train.shape(8000, 13)
- In [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
- In [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. - In [9]:
df_test.shape(2000, 12)
- In [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
EDA
- In [11]:
df_train['courier_trips_completed'].fillna(df_train['courier_trips_completed'].mean(),inplace=True) - In [12]:
df_train.dropna(inplace=True) - In [13]:
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
- In [14]:
df_train.dtypesorder_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
- In [15]:
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
- In [16]:
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
- In [17]:
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
- In [18]:
df_train.drop(columns=['order_id'],inplace=True) - In [19]:
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 - In [20]:
df_trainorder_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 - In [21]:
# count plot for category columns plt.figure(figsize=(10,10)) sns.countplot(x='cuisine',data=df_train,palette='Set2')<Axes: xlabel='cuisine', ylabel='count'> <Figure size 1000x1000 with 1 Axes>
- In [22]:
# count plot for courier_vehicle plt.figure(figsize=(10,10)) sns.countplot(x='courier_vehicle',data=df_train,palette='Set2')<Axes: xlabel='courier_vehicle', ylabel='count'> <Figure size 1000x1000 with 1 Axes>
- In [23]:
# count plot for city_zone plt.figure(figsize=(10,10)) sns.countplot(x='city_zone',data=df_train,palette='Set2')<Axes: xlabel='city_zone', ylabel='count'> <Figure size 1000x1000 with 1 Axes>
- In [24]:
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 - In [25]:
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()<Figure size 1000x1000 with 1 Axes>
- In [26]:
# dot plot plt.figure(figsize=(10,10)) sns.stripplot(x='courier_vehicle',y='delivery_minutes',data=df_train,palette='Set2')<Axes: xlabel='courier_vehicle', ylabel='delivery_minutes'> <Figure size 1000x1000 with 1 Axes>
- In [27]:
# 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
- In [28]:
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)<Axes: > <Figure size 1000x1000 with 2 Axes>
- In [29]:
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
- In [30]:
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
- In [31]:
df_train['hour']=pd.to_datetime(df_train['order_placed_at']).dt.hour - In [32]:
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
Feature Enginnering
- In [33]:
df_trainorder_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 - In [34]:
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.
<Figure size 1000x500 with 1 Axes>
- In [35]:
df_train['order_placed_at']=pd.to_datetime(df_train['order_placed_at']) - In [36]:
df_train['day_of_week'] = ( df_train['order_placed_at'].dt.dayofweek ) - In [37]:
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) - In [38]:
# 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()<Figure size 640x480 with 2 Axes>
- In [39]:
# 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) - In [40]:
# 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'] - In [41]:
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) - In [42]:
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) - In [43]:
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
- In [44]:
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()<Figure size 1400x600 with 2 Axes>
- In [45]:
correlation = df_train['hour_cos'].corr(df_train['delivery_minutes']) print(correlation)-0.02187353289435526
- In [46]:
# 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) - In [47]:
# 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']
- In [48]:
# drop the column as it doesnot have any effect or corelation on target features df_train.drop(columns=['hour'],inplace=True) - In [49]:
df_train.drop(columns=['day_of_week'],inplace=True) Peprocessing and training
- In [50]:
df_trainrestaurant_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 - In [51]:
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']
- In [52]:
# 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) - In [53]:
# X_train, X_val, y_train, y_val = train_test_split( # X, y, test_size=0.2, random_state=42 # ) - In [54]:
# # 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") - In [55]:
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']
- In [56]:
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 - In [57]:
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) - In [58]:
# 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 - In [59]:
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
Evaluation
- In [60]:
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
- In [61]:
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
- In [62]:
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']
- In [63]:
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']
Predection
- In [64]:
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 - In [65]:
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()
- In [66]:
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
- In [67]:
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
Gradient-Boosting
Ml Project 1
The brief this answers
Build and evaluate a model that predicts food delivery times, then explain what makes deliveries slow.
Try it yourselfOther 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.