Proyek Clustering: Beverage Sales¶
1. Perkenalan Dataset¶
Berdasarkan info dari kaggle, dataset ini merupakan data sintetis pola penjualan umum di industri minuman. Dengan total 8.999.910 entri dan 11 kolom, dataset ini menyajikan informasi yang cukup kaya untuk dianalisis dari berbagai sudut pandang.
Salah satu keunikan dari dataset ini adalah mencakup dua jenis transaksi:
- Business-to-Business (B2B)
- Business-to-Consumer (B2C)
Hal ini membuka peluang untuk melakukan analisis mendalam, seperti mengidentifikasi karakteristik pelanggan utama, produk dengan performa terbaik, serta tren penjualan musiman berdasarkan wilayah.
Namun, dalam proyek ini, fokus analisis adalah pada clustering berdasarkan produk dan hanya menggunakan 10.000 data sample untuk mengurangi beban preprocessing. Artinya, akan mencoba mengelompokkan produk berdasarkan kesamaan pola penjualannya. Tujuannya untuk memahami lebih dalam segmentasi produk yang mungkin memiliki strategi pemasaran atau penanganan stok yang berbeda.
Fitur | Deskripsi |
---|---|
Order_ID | ID unik untuk setiap pesanan. Satu pesanan dapat mencakup beberapa produk. |
Customer_ID | ID unik untuk setiap pelanggan yang membedakan antara satu pembeli dengan yang lain. |
Customer_Type | Jenis pelanggan, apakah B2B (bisnis ke bisnis) atau B2C (bisnis ke konsumen). |
Product | Nama produk yang dibeli, contohnya 'Coca-Cola' atau 'Erdinger Weißbier'. |
Category | Kategori produk, seperti 'Soft Drinks' (minuman ringan) atau 'Alcoholic Beverages' (minuman beralkohol). |
Unit_Price | Harga per unit dari produk tersebut. |
Quantity | Jumlah unit produk yang dibeli dalam satu pesanan. |
Discount | Diskon yang diterapkan pada produk (contoh: 0.1 berarti diskon 10%). Diskon hanya berlaku untuk pelanggan B2B. |
Total_Price | Total harga produk setelah diskon diterapkan. |
Region | Wilayah asal pelanggan, seperti 'Bayern' atau 'Berlin'. |
Order_Date | Tanggal ketika pesanan dilakukan. |
Sumber dataset: Beverage Sales Dataset
2. Import Library¶
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
from sklearn.preprocessing import MinMaxScaler
from sklearn.preprocessing import OneHotEncoder
from sklearn.decomposition import PCA
3. Memuat Dataset¶
df = pd.read_csv('synthetic_beverage_sales_data.csv')
df.head()
Order_ID | Customer_ID | Customer_Type | Product | Category | Unit_Price | Quantity | Discount | Total_Price | Region | Order_Date | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | ORD1 | CUS1496 | B2B | Vio Wasser | Water | 1.66 | 53 | 0.10 | 79.18 | Baden-Württemberg | 2023-08-23 |
1 | ORD1 | CUS1496 | B2B | Evian | Water | 1.56 | 90 | 0.10 | 126.36 | Baden-Württemberg | 2023-08-23 |
2 | ORD1 | CUS1496 | B2B | Sprite | Soft Drinks | 1.17 | 73 | 0.05 | 81.14 | Baden-Württemberg | 2023-08-23 |
3 | ORD1 | CUS1496 | B2B | Rauch Multivitamin | Juices | 3.22 | 59 | 0.10 | 170.98 | Baden-Württemberg | 2023-08-23 |
4 | ORD1 | CUS1496 | B2B | Gerolsteiner | Water | 0.87 | 35 | 0.10 | 27.40 | Baden-Württemberg | 2023-08-23 |
4. Exploratory Data Analysis (EDA)¶
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 8999910 entries, 0 to 8999909 Data columns (total 11 columns): # Column Dtype --- ------ ----- 0 Order_ID object 1 Customer_ID object 2 Customer_Type object 3 Product object 4 Category object 5 Unit_Price float64 6 Quantity int64 7 Discount float64 8 Total_Price float64 9 Region object 10 Order_Date object dtypes: float64(3), int64(1), object(7) memory usage: 755.3+ MB
df.head()
Order_ID | Customer_ID | Customer_Type | Product | Category | Unit_Price | Quantity | Discount | Total_Price | Region | Order_Date | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | ORD1 | CUS1496 | B2B | Vio Wasser | Water | 1.66 | 53 | 0.10 | 79.18 | Baden-Württemberg | 2023-08-23 |
1 | ORD1 | CUS1496 | B2B | Evian | Water | 1.56 | 90 | 0.10 | 126.36 | Baden-Württemberg | 2023-08-23 |
2 | ORD1 | CUS1496 | B2B | Sprite | Soft Drinks | 1.17 | 73 | 0.05 | 81.14 | Baden-Württemberg | 2023-08-23 |
3 | ORD1 | CUS1496 | B2B | Rauch Multivitamin | Juices | 3.22 | 59 | 0.10 | 170.98 | Baden-Württemberg | 2023-08-23 |
4 | ORD1 | CUS1496 | B2B | Gerolsteiner | Water | 0.87 | 35 | 0.10 | 27.40 | Baden-Württemberg | 2023-08-23 |
df = df.sample(n=10000, random_state=42, ignore_index=True)
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 10000 entries, 0 to 9999 Data columns (total 11 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Order_ID 10000 non-null object 1 Customer_ID 10000 non-null object 2 Customer_Type 10000 non-null object 3 Product 10000 non-null object 4 Category 10000 non-null object 5 Unit_Price 10000 non-null float64 6 Quantity 10000 non-null int64 7 Discount 10000 non-null float64 8 Total_Price 10000 non-null float64 9 Region 10000 non-null object 10 Order_Date 10000 non-null object dtypes: float64(3), int64(1), object(7) memory usage: 859.5+ KB
df['Order_Date'] = pd.to_datetime(df['Order_Date'])
⚠️ Terdapat fitur Order_Date yang memiliki type data yang tidak sesuai. Sehingga type data ini langsung diperbaiki karena dapat mempengaruhi ketika membuat visualisasi jika menggunakan kolom ini nantinya.
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 10000 entries, 0 to 9999 Data columns (total 11 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Order_ID 10000 non-null object 1 Customer_ID 10000 non-null object 2 Customer_Type 10000 non-null object 3 Product 10000 non-null object 4 Category 10000 non-null object 5 Unit_Price 10000 non-null float64 6 Quantity 10000 non-null int64 7 Discount 10000 non-null float64 8 Total_Price 10000 non-null float64 9 Region 10000 non-null object 10 Order_Date 10000 non-null datetime64[ns] dtypes: datetime64[ns](1), float64(3), int64(1), object(6) memory usage: 859.5+ KB
Missing Valus¶
missing_values = df.isnull().sum()
print('Jumlah missing values kolom:')
print(missing_values)
Jumlah missing values kolom: Order_ID 0 Customer_ID 0 Customer_Type 0 Product 0 Category 0 Unit_Price 0 Quantity 0 Discount 0 Total_Price 0 Region 0 Order_Date 0 dtype: int64
Duplicate Data¶
duplicates = df.duplicated()
print(f'Jumlah data duplikat: {duplicates.sum()}')
Jumlah data duplikat: 0
✅ Tidak ada missing dan duplicate. Sehingga tidak ada tahapan drop data pada preprocessing nantinya.
df.describe()
Unit_Price | Quantity | Discount | Total_Price | Order_Date | |
---|---|---|---|---|---|
count | 10000.000000 | 10000.000000 | 10000.000000 | 10000.000000 | 10000 |
mean | 5.876568 | 22.787500 | 0.029495 | 135.385663 | 2022-06-30 14:13:55.200000256 |
min | 0.330000 | 1.000000 | 0.000000 | 0.350000 | 2021-01-01 00:00:00 |
25% | 1.050000 | 6.000000 | 0.000000 | 8.300000 | 2021-09-20 00:00:00 |
50% | 1.750000 | 11.000000 | 0.000000 | 20.700000 | 2022-06-28 12:00:00 |
75% | 3.190000 | 29.000000 | 0.050000 | 67.842500 | 2023-04-08 00:00:00 |
max | 138.720000 | 100.000000 | 0.150000 | 10111.960000 | 2023-12-30 00:00:00 |
std | 15.093122 | 26.569801 | 0.044710 | 528.922868 | NaN |
💡 Insight Awal
Hasil ringkasan statistik dari 10.000 sampel:
Unit Price
- Rata-rata harga unit produk adalah 138,72!
- 50% produk dijual dengan harga di bawah $1,75, artinya sebagian besar produk cenderung murah—mungkin minuman kemasan atau produk kecil lainnya.
- Rentang harga yang luas ini menunjukkan kalau ada kombinasi produk murah dan premium di dalam dataset.
Quantity
- Rata-rata jumlah pembelian per transaksi adalah sekitar 22 item, dengan maksimum sampai 100 item.
- Tapi kalau lihat dari median-nya (50%), kebanyakan transaksi cuma sekitar 11 item. Jadi, ada beberapa transaksi besar yang “menarik” rata-ratanya naik.
- Ini bisa jadi indikasi bahwa ada perbedaan signifikan antara pembelian individu dan pembelian dalam jumlah besar (mungkin dari B2B).
Discount
- Diskon rata-rata cukup kecil, hanya ~2,95%, dan mayoritas transaksi ternyata nggak pakai diskon sama sekali (median = 0).
- Diskon maksimal yang tercatat cuma 15%, artinya strategi diskon di dataset ini cukup konservatif.
- Bisa diasumsikan, produk-produk tertentu aja yang dikasih diskon, atau hanya berlaku untuk pelanggan tertentu (misal B2B).
Total Price
- Rata-rata nilai transaksi ada di Rp135,39, tapi ini juga dipengaruhi oleh transaksi besar—karena nilai maksimum mencapai Rp10.111,96.
- 75% transaksi ada di bawah Rp67,84, jadi transaksi dengan nilai tinggi adalah outlier atau kasus khusus.
✨ Kesimpulan Sementara:
Kebanyakan produk yang dijual harganya tergolong murah, dengan pembelian dalam jumlah kecil sampai menengah.
Diskon bukan faktor dominan, sehingga kemungkinan besar strategi harga lebih mengandalkan volume penjualan atau segmentasi pelanggan.
Variasi tinggi dalam harga unit dan total transaksi menunjukkan adanya beragam jenis produk dan pelanggan—sangat cocok untuk dilakukan clustering agar bisa menemukan pola yang lebih spesifik.
sns.set(style='whitegrid')
plt.rcParams['figure.figsize'] = (15, 8)
# Tren Penjualan Harian
df_daily = df.groupby('Order_Date')['Total_Price'].sum().reset_index()
plt.figure(figsize=(15, 5))
sns.lineplot(data=df_daily, x='Order_Date', y='Total_Price')
plt.title('Tren Total Penjualan Harian')
plt.xlabel('Tanggal')
plt.ylabel('Total Penjualan')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
💡 Trend Penjualan
Tren total penjualan harian dari tahun 2021 sampai awal 2024. Kalau dilihat, penjualannya cukup fluktuatif naik turun terus tiap hari, dengan beberapa lonjakan besar yang muncul di waktu-waktu tertentu. Lonjakan ini kemungkinan besar terjadi pas ada promo, event khusus, atau momen liburan. Tapi secara keseluruhan, tidak terlalu terlihat ada tren yang benar-benar naik atau turun terus-menerus, jadi bisa dibilang pola penjualannya lebih dipengaruhi momen-momen tertentu daripada tren musiman yang stabil.
# Distribusi Fitur Numerik
numerical_cols = ['Unit_Price', 'Quantity', 'Discount', 'Total_Price']
fig, axs = plt.subplots(2, 2, figsize=(15, 10))
fig.suptitle('Distribusi Fitur Numerik', fontsize=16)
for i, col in enumerate(numerical_cols):
ax = axs[i // 2, i % 2]
sns.histplot(df[col], kde=True, bins=30, ax=ax)
ax.set_title(f'{col}')
plt.tight_layout(rect=[0, 0, 1, 0.95])
plt.show()
# Boxplot Fitur Numerik
fig, axs = plt.subplots(2, 2, figsize=(15, 10))
fig.suptitle('Boxplot Fitur Numerik (Deteksi Outlier)', fontsize=16)
for i, col in enumerate(numerical_cols):
ax = axs[i // 2, i % 2]
sns.boxplot(data=df, x=col, ax=ax)
ax.set_title(f'{col}')
plt.tight_layout(rect=[0, 0, 1, 0.95])
plt.show()
💡
Unit_Price memiliki banyak nilai ekstrem tinggi di luar whisker atas, sedangkan Quantity menunjukkan beberapa nilai tinggi yang mencolok. Pada Discount, terdapat beberapa nilai yang tidak biasa di ujung atas, yang mungkin mengindikasikan kebijakan diskon besar-besaran. Sementara itu, Total_Price memiliki jumlah outlier paling signifikan, dengan nilai yang sangat tinggi.
# Korelasi
plt.figure(figsize=(8, 6))
sns.heatmap(df[numerical_cols].corr(), annot=True, cmap='coolwarm', fmt='.2f')
plt.title('Korelasi Antar Fitur Numerik')
plt.show()
💡 Korelasi Fitur
Terlihat bahwa Unit_Price punya korelasi yang cukup kuat dengan Total_Price (0.65), yang masuk akal karena harga satuan jelas berpengaruh terhadap total harga. Quantity dan Discount juga punya korelasi tinggi satu sama lain (0.82), mungkin karena diskon cenderung lebih besar kalau jumlah pembelian banyak. Sementara itu, Discount dan Total_Price punya korelasi rendah (0.25), yang bisa jadi karena efek diskon nggak terlalu besar terhadap total harga dibandingkan jumlah dan harga satuan.
# Distribusi Kategorikal
categorical_cols = ['Customer_Type', 'Category', 'Region']
fig, axs = plt.subplots(1, 3, figsize=(18, 5))
fig.suptitle('Distribusi Fitur Kategorikal', fontsize=16)
for i, col in enumerate(categorical_cols):
sns.countplot(data=df, x=col, order=df[col].value_counts().index, ax=axs[i])
axs[i].set_title(col)
axs[i].tick_params(axis='x', rotation=45)
plt.tight_layout(rect=[0, 0, 1, 0.92])
plt.show()
💡 Analisis Data Kategori
Dari sisi tipe pelanggan, mayoritas transaksi berasal dari pelanggan B2C, yang jumlahnya hampir dua kali lipat dibandingkan B2B. Untuk kategori produk, distribusinya cukup seimbang antara Water, Alcoholic Beverages, Juices, dan Soft Drinks, yang menunjukkan tidak ada kategori yang terlalu dominan. Sementara itu, distribusi berdasarkan wilayah (Region) terlihat cukup merata, meskipun Hamburg memiliki jumlah transaksi paling tinggi, diikuti oleh Niedersachsen dan Nordrhein-Westfalen. Beberapa wilayah seperti Brandenburg dan Thuringen berada di posisi terbawah, tapi secara keseluruhan, penyebaran transaksi antar wilayah relatif seimbang.
# Top Region dan Product
top_regions = df.groupby('Region')['Total_Price'].sum().sort_values(ascending=False).head(5).reset_index()
top_products = df.groupby('Product')['Quantity'].sum().sort_values(ascending=False).head(10).reset_index()
fig, axs = plt.subplots(1, 3, figsize=(22, 6))
fig.suptitle('Ringkasan Penjualan: Customer Type, Region, dan Produk Terlaris', fontsize=18)
# Total Penjualan per Customer Type
sns.barplot(data=df, x='Customer_Type', y='Total_Price', estimator=sum, ax=axs[0])
axs[0].set_title('Total Penjualan per Customer Type')
axs[0].set_xlabel('Customer Type')
axs[0].set_ylabel('Total Penjualan')
# Top 5 Region berdasarkan Total Penjualan
sns.barplot(data=top_regions, x='Region', y='Total_Price', ax=axs[1])
axs[1].set_title('Top 5 Region berdasarkan Total Penjualan')
axs[1].set_xlabel('Region')
axs[1].set_ylabel('Total Penjualan')
axs[1].tick_params(axis='x', rotation=45)
# Top 10 Produk Terlaris
sns.barplot(
data=top_products,
x='Quantity', y='Product',
ax=axs[2]
)
axs[2].set_title('Top 10 Produk Terlaris (Berdasarkan Quantity)')
axs[2].set_xlabel('Total Quantity')
axs[2].set_ylabel('Product')
plt.tight_layout(rect=[0, 0, 1, 0.93])
plt.show()
💡Analisis Penjualan
Terlihat bahwa meskipun jumlah pelanggan B2C lebih banyak, kontribusi penjualan terbesar justru berasal dari segmen B2B dengan selisih yang cukup signifikan. Dari sisi wilayah, Hamburg menempati posisi teratas dalam total penjualan, diikuti oleh Nordrhein-Westfalen dan Saarland. Sementara itu, produk-produk jus seperti Hohes C Orange, Cranberry Juice, dan Passion Fruit Juice menjadi produk paling laris berdasarkan total kuantitas penjualan. Hal ini menunjukkan bahwa penjualan sangat dipengaruhi oleh jenis pelanggan dan preferensi produk tertentu.
Multivariate Analysis¶
sns.pairplot(df, vars=df.select_dtypes(include=['float64', 'int64']))
plt.show()
5. Data Preprocessing¶
Penanganan Outlier¶
def remove_outliers_iqr(df, columns, verbose=True):
# Hitung Q1, Q3, dan IQR
Q1 = df[columns].quantile(0.25)
Q3 = df[columns].quantile(0.75)
IQR = Q3 - Q1
# Hitung batas bawah dan atas
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
# Persentase outlier per kolom
if verbose:
print('Persentase outlier per kolom:')
for col in columns:
outliers = df[(df[col] < lower_bound[col]) | (df[col] > upper_bound[col])]
percent = (len(outliers) / len(df)) * 100
print(f' - {col}: {percent:.2f}% ({len(outliers)} dari {len(df)} baris)')
# Buat mask gabungan untuk menghapus baris dengan outlier di salah satu kolom
mask = ~((df[columns] < lower_bound) | (df[columns] > upper_bound)).any(axis=1)
cleaned_df = df[mask]
if verbose:
total_outliers = len(df) - len(cleaned_df)
percent_total = (total_outliers / len(df)) * 100
print(f'\nTotal baris yang dihapus karena outlier: {total_outliers} ({percent_total:.2f}%)')
print(f'Jumlah data setelah pembersihan: {len(cleaned_df)}')
return cleaned_df
# Hapus outlier
numerical_cols = ['Unit_Price', 'Quantity', 'Discount', 'Total_Price']
df_clean = remove_outliers_iqr(df, numerical_cols)
Persentase outlier per kolom: - Unit_Price: 12.08% (1208 dari 10000 baris) - Quantity: 12.64% (1264 dari 10000 baris) - Discount: 4.31% (431 dari 10000 baris) - Total_Price: 12.93% (1293 dari 10000 baris) Total baris yang dihapus karena outlier: 2449 (24.49%) Jumlah data setelah pembersihan: 7551
# Visualisasi Setelah Outlier Dihapus
fig, axs = plt.subplots(1, len(numerical_cols), figsize=(18, 5))
fig.suptitle('Boxplot Setelah Menghapus Outlier (IQR Method)', fontsize=16)
for i, col in enumerate(numerical_cols):
sns.boxplot(data=df_clean, y=col, ax=axs[i], color='skyblue')
axs[i].set_title(col)
axs[i].set_ylabel('')
plt.tight_layout(rect=[0, 0, 1, 0.93])
plt.show()
df_cleaned = df_clean.copy()
df_cleaned.info()
<class 'pandas.core.frame.DataFrame'> Index: 7551 entries, 1 to 9999 Data columns (total 11 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Order_ID 7551 non-null object 1 Customer_ID 7551 non-null object 2 Customer_Type 7551 non-null object 3 Product 7551 non-null object 4 Category 7551 non-null object 5 Unit_Price 7551 non-null float64 6 Quantity 7551 non-null int64 7 Discount 7551 non-null float64 8 Total_Price 7551 non-null float64 9 Region 7551 non-null object 10 Order_Date 7551 non-null datetime64[ns] dtypes: datetime64[ns](1), float64(3), int64(1), object(6) memory usage: 707.9+ KB
df_cleaned = df_cleaned.sort_values(by='Order_ID', ascending=True).reset_index(drop=True)
df_cleaned
Order_ID | Customer_ID | Customer_Type | Product | Category | Unit_Price | Quantity | Discount | Total_Price | Region | Order_Date | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | ORD100001 | CUS1112 | B2C | Mango Juice | Juices | 3.12 | 1 | 0.00 | 3.12 | Sachsen | 2021-03-16 |
1 | ORD1000934 | CUS9934 | B2B | Cranberry Juice | Juices | 3.41 | 26 | 0.10 | 79.79 | Nordrhein-Westfalen | 2021-11-11 |
2 | ORD100115 | CUS3763 | B2C | Mango Juice | Juices | 3.21 | 8 | 0.00 | 25.68 | Sachsen-Anhalt | 2022-05-14 |
3 | ORD1001639 | CUS8574 | B2C | Jever | Alcoholic Beverages | 1.99 | 13 | 0.00 | 25.87 | Bayern | 2023-03-25 |
4 | ORD1001725 | CUS911 | B2B | Cranberry Juice | Juices | 3.37 | 9 | 0.05 | 28.81 | Hamburg | 2021-03-24 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
7546 | ORD996717 | CUS6636 | B2C | Fritz-Kola | Soft Drinks | 2.05 | 10 | 0.00 | 20.50 | Saarland | 2022-04-05 |
7547 | ORD99765 | CUS7034 | B2C | Mezzo Mix | Soft Drinks | 0.59 | 13 | 0.00 | 7.67 | Baden-Württemberg | 2021-08-03 |
7548 | ORD998482 | CUS2401 | B2C | Riesling | Alcoholic Beverages | 4.07 | 2 | 0.00 | 8.14 | Thüringen | 2021-09-21 |
7549 | ORD998988 | CUS9257 | B2C | Pepsi | Soft Drinks | 1.68 | 11 | 0.00 | 18.48 | Brandenburg | 2022-07-01 |
7550 | ORD999151 | CUS4996 | B2C | Merlot | Alcoholic Beverages | 4.35 | 8 | 0.00 | 34.80 | Bremen | 2022-08-03 |
7551 rows × 11 columns
Normalisasi dan Standarisasi Fitur¶
# Fitur yang digunakan
categoric_cols = ['Category', 'Region', 'Customer_Type']
numeric_cols = ['Unit_Price', 'Quantity', 'Total_Price', 'Discount']
# Inisiasi Encoder dan Scaler
encoder = OneHotEncoder(sparse_output=False, handle_unknown='ignore')
scaler = MinMaxScaler()
# Fitur Kategori
encoded_array = encoder.fit_transform(df_cleaned[categoric_cols])
encoded_df = pd.DataFrame(encoded_array, columns=encoder.get_feature_names_out(categoric_cols))
# Fitur Numerik
df_numerik_scaled = pd.DataFrame(
scaler.fit_transform(df_cleaned[numeric_cols]),
columns=numeric_cols
)
df_combine.head()
Unit_Price | Quantity | Total_Price | Discount | Category_Alcoholic Beverages | Category_Juices | Category_Soft Drinks | Category_Water | Region_Baden-Württemberg | Region_Bayern | ... | Region_Niedersachsen | Region_Nordrhein-Westfalen | Region_Rheinland-Pfalz | Region_Saarland | Region_Sachsen | Region_Sachsen-Anhalt | Region_Schleswig-Holstein | Region_Thüringen | Customer_Type_B2B | Customer_Type_B2C | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0.468121 | 0.000000 | 0.017945 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 |
1 | 0.516779 | 0.403226 | 0.514641 | 1.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 |
2 | 0.483221 | 0.112903 | 0.164097 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 1.0 |
3 | 0.278523 | 0.193548 | 0.165328 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 |
4 | 0.510067 | 0.129032 | 0.184374 | 0.5 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 |
5 rows × 26 columns
6. Pembangunan Model Clustering¶
a. Pembangunan Model Clustering¶
inertia = []
K_range = range(2, 10)
for k in K_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
kmeans.fit(df_combine)
inertia.append(kmeans.inertia_)
# Plot Elbow Method
plt.figure(figsize=(8, 5))
plt.plot(K_range, inertia, marker='o', linestyle='--')
plt.xlabel('Number of Clusters')
plt.ylabel('Inertia (SSE)')
plt.title('Elbow Method for Optimal K')
plt.show()
💡 Berdasarkan hasil visualisasi, maka K=6 diambil sebagai jumlah cluster yang optimal karena pada titik tersebut terjadi penurunan inertia yang mulai melambat (elbow point), sehingga penambahan jumlah cluster setelah K=6 tidak memberikan pengurangan inertia yang signifikan.
# K-Means Clustering
optimal_k = 6
kmeans = KMeans(n_clusters=optimal_k, random_state=42, n_init=10)
df_combine['Cluster'] = kmeans.fit_predict(df_combine)
b. Evaluasi Model Clustering¶
silhouette_avg = silhouette_score(df_combine, kmeans.labels_)
print(f'Silhouette Score: {silhouette_avg}')
Silhouette Score: 0.37988763640929135
c. Feature Selection (Opsional)¶
features_for_pca = df_combine.drop(columns=['Cluster'])
inertia = []
K_range = range(2, 11)
for k in K_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
kmeans.fit(features_for_pca)
inertia.append(kmeans.inertia_)
# Plot Elbow Curve
plt.figure(figsize=(8, 5))
plt.plot(K_range, inertia, 'bo-')
plt.xlabel('Jumlah Klaster (k)')
plt.ylabel('Inertia')
plt.title('Elbow Method')
plt.grid(True)
plt.show()
Hasil n_cluster optimal masih yang sama yaitu 6.
# PCA
pca = PCA(n_components=2)
X_pca = pca.fit_transform(features_for_pca)
# Clustering setelah PCA
kmeans_final = KMeans(n_clusters=6, random_state=42, n_init=10)
clusters = kmeans_final.fit_predict(X_pca)
silhouette_avg_pca = silhouette_score(X_pca, clusters)
print(f'Silhouette Score setelah PCA: {silhouette_avg_pca:.3f}')
Silhouette Score setelah PCA: 0.879
d. Visualisasi Hasil Clustering¶
df_combine['Cluster_PCA'] = clusters
df_combine
Unit_Price | Quantity | Total_Price | Discount | Category_Alcoholic Beverages | Category_Juices | Category_Soft Drinks | Category_Water | Region_Baden-Württemberg | Region_Bayern | ... | Region_Rheinland-Pfalz | Region_Saarland | Region_Sachsen | Region_Sachsen-Anhalt | Region_Schleswig-Holstein | Region_Thüringen | Customer_Type_B2B | Customer_Type_B2C | Cluster | Cluster_PCA | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0.468121 | 0.000000 | 0.017945 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 2 | 4 |
1 | 0.516779 | 0.403226 | 0.514641 | 1.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 3 | 3 |
2 | 0.483221 | 0.112903 | 0.164097 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 1.0 | 2 | 4 |
3 | 0.278523 | 0.193548 | 0.165328 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0 | 1 |
4 | 0.510067 | 0.129032 | 0.184374 | 0.5 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 3 | 3 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
7546 | 0.288591 | 0.145161 | 0.130539 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 1 | 1 |
7547 | 0.043624 | 0.193548 | 0.047422 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 1.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 1 | 1 |
7548 | 0.627517 | 0.016129 | 0.050466 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 1.0 | 0 | 1 |
7549 | 0.226510 | 0.161290 | 0.117453 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 1 | 1 |
7550 | 0.674497 | 0.112903 | 0.223180 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0 | 1 |
7551 rows × 28 columns
e. Analisis dan Interpretasi Hasil Cluster¶
# invers data
df_num_normalize = df_combine[numeric_cols]
df_num_inverse = pd.DataFrame(scaler.inverse_transform(df_num_normalize), columns=numeric_cols)
# Tambah kembali kolom Cluster
df_final['Cluster'] = df_combine['Cluster'].values
df_final['Cluster_PCA'] = df_combine['Cluster_PCA'].values
df_final.head(2)
Order_ID | Customer_ID | Product | Order_Date | Unit_Price | Quantity | Total_Price | Discount | Category | Region | Customer_Type | Cluster | Cluster_PCA | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | ORD100001 | CUS1112 | Mango Juice | 2021-03-16 | 3.12 | 1.0 | 3.12 | 0.0 | Juices | Sachsen | B2C | 2 | 4 |
1 | ORD1000934 | CUS9934 | Cranberry Juice | 2021-11-11 | 3.41 | 26.0 | 79.79 | 0.1 | Juices | Nordrhein-Westfalen | B2B | 3 | 3 |
order_column = [
'Order_ID', 'Customer_ID', 'Customer_Type', 'Product', 'Category',
'Unit_Price', 'Quantity', 'Discount', 'Total_Price', 'Region', 'Order_Date', 'Cluster', 'Cluster_PCA'
]
df_final = df_final.reindex(columns=order_column)
df_final.head(2)
Order_ID | Customer_ID | Customer_Type | Product | Category | Unit_Price | Quantity | Discount | Total_Price | Region | Order_Date | Cluster | Cluster_PCA | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | ORD100001 | CUS1112 | B2C | Mango Juice | Juices | 3.12 | 1.0 | 0.0 | 3.12 | Sachsen | 2021-03-16 | 2 | 4 |
1 | ORD1000934 | CUS9934 | B2B | Cranberry Juice | Juices | 3.41 | 26.0 | 0.1 | 79.79 | Nordrhein-Westfalen | 2021-11-11 | 3 | 3 |
df_cleaned.head(2)
Order_ID | Customer_ID | Customer_Type | Product | Category | Unit_Price | Quantity | Discount | Total_Price | Region | Order_Date | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | ORD100001 | CUS1112 | B2C | Mango Juice | Juices | 3.12 | 1 | 0.0 | 3.12 | Sachsen | 2021-03-16 |
1 | ORD1000934 | CUS9934 | B2B | Cranberry Juice | Juices | 3.41 | 26 | 0.1 | 79.79 | Nordrhein-Westfalen | 2021-11-11 |
df_final.tail(2)
Order_ID | Customer_ID | Customer_Type | Product | Category | Unit_Price | Quantity | Discount | Total_Price | Region | Order_Date | Cluster | Cluster_PCA | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
7549 | ORD998988 | CUS9257 | B2C | Pepsi | Soft Drinks | 1.68 | 11.0 | 0.0 | 18.48 | Brandenburg | 2022-07-01 | 1 | 1 |
7550 | ORD999151 | CUS4996 | B2C | Merlot | Alcoholic Beverages | 4.35 | 8.0 | 0.0 | 34.80 | Bremen | 2022-08-03 | 0 | 1 |
df_cleaned.tail(2)
Order_ID | Customer_ID | Customer_Type | Product | Category | Unit_Price | Quantity | Discount | Total_Price | Region | Order_Date | |
---|---|---|---|---|---|---|---|---|---|---|---|
7549 | ORD998988 | CUS9257 | B2C | Pepsi | Soft Drinks | 1.68 | 11 | 0.0 | 18.48 | Brandenburg | 2022-07-01 |
7550 | ORD999151 | CUS4996 | B2C | Merlot | Alcoholic Beverages | 4.35 | 8 | 0.0 | 34.80 | Bremen | 2022-08-03 |
✅ Berhasil menggabungkan kembali secara utuh, tidak adanya perbedaan antara data awal (df_clean) dengan data akhir (df_final)
df_final
Order_ID | Customer_ID | Customer_Type | Product | Category | Unit_Price | Quantity | Discount | Total_Price | Region | Order_Date | Cluster | Cluster_PCA | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | ORD100001 | CUS1112 | B2C | Mango Juice | Juices | 3.12 | 1.0 | 0.00 | 3.12 | Sachsen | 2021-03-16 | 2 | 4 |
1 | ORD1000934 | CUS9934 | B2B | Cranberry Juice | Juices | 3.41 | 26.0 | 0.10 | 79.79 | Nordrhein-Westfalen | 2021-11-11 | 3 | 3 |
2 | ORD100115 | CUS3763 | B2C | Mango Juice | Juices | 3.21 | 8.0 | 0.00 | 25.68 | Sachsen-Anhalt | 2022-05-14 | 2 | 4 |
3 | ORD1001639 | CUS8574 | B2C | Jever | Alcoholic Beverages | 1.99 | 13.0 | 0.00 | 25.87 | Bayern | 2023-03-25 | 0 | 1 |
4 | ORD1001725 | CUS911 | B2B | Cranberry Juice | Juices | 3.37 | 9.0 | 0.05 | 28.81 | Hamburg | 2021-03-24 | 3 | 3 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
7546 | ORD996717 | CUS6636 | B2C | Fritz-Kola | Soft Drinks | 2.05 | 10.0 | 0.00 | 20.50 | Saarland | 2022-04-05 | 1 | 1 |
7547 | ORD99765 | CUS7034 | B2C | Mezzo Mix | Soft Drinks | 0.59 | 13.0 | 0.00 | 7.67 | Baden-Württemberg | 2021-08-03 | 1 | 1 |
7548 | ORD998482 | CUS2401 | B2C | Riesling | Alcoholic Beverages | 4.07 | 2.0 | 0.00 | 8.14 | Thüringen | 2021-09-21 | 0 | 1 |
7549 | ORD998988 | CUS9257 | B2C | Pepsi | Soft Drinks | 1.68 | 11.0 | 0.00 | 18.48 | Brandenburg | 2022-07-01 | 1 | 1 |
7550 | ORD999151 | CUS4996 | B2C | Merlot | Alcoholic Beverages | 4.35 | 8.0 | 0.00 | 34.80 | Bremen | 2022-08-03 | 0 | 1 |
7551 rows × 13 columns
# Agregasi numerik per cluster
agg_numeric = df_final.groupby("Cluster_PCA").agg({
"Unit_Price": "mean",
"Quantity": "mean",
"Discount": "mean",
"Total_Price": ["mean", "sum"],
"Customer_ID": pd.Series.nunique,
"Order_ID": "count"
})
agg_numeric.columns = ['Avg_Unit_Price', 'Avg_Quantity', 'Avg_Discount', 'Avg_Total_Price', 'Total_Sales', 'Unique_Customers', 'Total_Orders']
agg_numeric.reset_index(inplace=True)
# Agregasi kategorik (nilai yang paling sering muncul per cluster)
agg_categorical = df_final.groupby("Cluster_PCA").agg({
"Product": lambda x: x.value_counts().idxmax(),
"Category": lambda x: x.value_counts().idxmax(),
"Customer_Type": lambda x: x.value_counts().idxmax(),
"Region": lambda x: x.value_counts().idxmax()
}).reset_index()
# Gabungkan hasil numerik dan kategorik
agg_summary = pd.merge(agg_numeric, agg_categorical, on="Cluster_PCA")
agg_summary
Cluster_PCA | Avg_Unit_Price | Avg_Quantity | Avg_Discount | Avg_Total_Price | Total_Sales | Unique_Customers | Total_Orders | Product | Category | Customer_Type | Region | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0.827370 | 31.929630 | 0.065000 | 25.597833 | 13822.83 | 505 | 540 | Selters | Water | B2B | Hamburg |
1 | 1 | 1.861303 | 8.010146 | 0.000000 | 15.159724 | 37353.56 | 2039 | 2464 | Fritz-Kola | Soft Drinks | B2C | Bremen |
2 | 2 | 0.986252 | 8.116082 | 0.000000 | 8.039528 | 13297.38 | 1465 | 1654 | San Pellegrino | Water | B2C | Niedersachsen |
3 | 3 | 2.330077 | 29.195358 | 0.069149 | 62.463540 | 32293.65 | 479 | 517 | Hohes C Orange | Juices | B2B | Nordrhein-Westfalen |
4 | 4 | 2.707812 | 7.927906 | 0.000000 | 21.517999 | 34622.46 | 1422 | 1609 | Mango Juice | Juices | B2C | Hamburg |
5 | 5 | 1.639309 | 30.405476 | 0.066949 | 45.129348 | 34614.21 | 698 | 767 | Fritz-Kola | Soft Drinks | B2B | Mecklenburg-Vorpommern |
Analisis Karakteristik Cluster dari Model KMeans¶
Cluster 0 – B2B Pembeli Air Mineral dalam Volume Tinggi (Hamburg)¶
- Harga Satuan Rata-Rata: Sangat rendah (Rp0.83)
- Jumlah Pembelian Rata-Rata: Sangat tinggi (31.93 unit per transaksi)
- Diskon Rata-Rata: Sedikit (6.5%)
- Total per Transaksi: Cukup tinggi (Rp25.60)
- Total Penjualan: Rp13.823,83
- Jumlah Pelanggan Unik: 505
- Jumlah Pesanan: 540
- Produk Dominan: Selters
- Kategori: Water
- Tipe Pelanggan: B2B
- Wilayah: Hamburg
Interpretasi:
Cluster ini mewakili pelanggan bisnis (restoran, hotel, event organizer) yang membeli air mineral dalam jumlah besar dengan harga grosir dan sedikit diskon. Ini menunjukkan pembelian reguler dalam jumlah besar untuk kebutuhan operasional.
Cluster 1 – Konsumen B2C Ritel Minuman Ringan (Bremen)¶
- Harga Satuan Rata-Rata: Sedang (Rp1.86)
- Jumlah Pembelian Rata-Rata: Rendah (8 unit)
- Diskon: Tidak ada
- Total per Transaksi: Rp15.16
- Total Penjualan: Rp37.353,56
- Jumlah Pelanggan Unik: 2.039
- Jumlah Pesanan: 2.464
- Produk Dominan: Fritz-Kola
- Kategori: Soft Drinks
- Tipe Pelanggan: B2C
- Wilayah: Bremen
Interpretasi:
Ini adalah pelanggan ritel B2C yang membeli minuman ringan dalam jumlah kecil tanpa diskon, kemungkinan pembeli individu atau keluarga. Tingginya total penjualan dan jumlah order menunjukkan segmentasi pasar yang luas dan stabil.
Cluster 2 – B2C Air Mineral Premium Volume Rendah (Niedersachsen)¶
- Harga Satuan: Rendah ke sedang (Rp0.99)
- Jumlah Pembelian: Kecil (8.1 unit)
- Diskon: Tidak ada
- Total per Transaksi: Rendah (Rp8.04)
- Total Penjualan: Rp13.297,38
- Jumlah Pelanggan Unik: 1.465
- Jumlah Pesanan: 1.654
- Produk Favorit: San Pellegrino
- Kategori: Water
- Tipe Pelanggan: B2C
- Wilayah: Niedersachsen
Interpretasi:
Konsumen pribadi yang membeli air premium dalam jumlah kecil dan tanpa diskon. Karakter ini cenderung loyal dan lebih memilih kualitas dibandingkan harga. Meski jumlah transaksi besar, nilai per transaksi tetap rendah.
Cluster 3 – B2B Pembeli Jus Volume Tinggi dengan Diskon (Nordrhein-Westfalen)¶
- Harga Satuan: Tinggi (Rp2.33)
- Jumlah Pembelian: Tinggi (29.2 unit)
- Diskon: Cukup tinggi (6.91%)
- Total per Transaksi: Tertinggi (Rp62.46)
- Total Penjualan: Rp32.293,65
- Jumlah Pelanggan Unik: 479
- Jumlah Pesanan: 517
- Produk Favorit: Hohes C Orange
- Kategori: Juices
- Tipe Pelanggan: B2B
- Wilayah: Nordrhein-Westfalen
Interpretasi:
Ini adalah segmen pelanggan bisnis besar seperti hotel atau katering yang membeli jus dalam jumlah besar dan mendapatkan diskon tinggi. Nilai transaksi sangat tinggi, menunjukkan pembelian dengan intensitas tinggi dan volume besar.
Cluster 4 – B2C Konsumen Jus Premium Berkelas (Hamburg)¶
- Harga Satuan: Tertinggi (Rp2.71)
- Jumlah Pembelian: Rendah (7.93)
- Diskon: Tidak ada
- Total per Transaksi: Rp21.52
- Total Penjualan: Rp34.622,46
- Jumlah Pelanggan Unik: 1.422
- Jumlah Pesanan: 1.609
- Produk Favorit: Mango Juice
- Kategori: Juices
- Tipe Pelanggan: B2C
- Wilayah: Hamburg
Interpretasi:
Pelanggan pribadi dengan daya beli tinggi, membeli jus premium dengan harga tertinggi di antara semua cluster. Tidak adanya diskon menunjukkan bahwa mereka kurang sensitif terhadap harga, mungkin berasal dari kalangan kelas menengah ke atas.
Cluster 5 – B2B Pembeli Soft Drinks Volume Tinggi (Mecklenburg-Vorpommern)¶
- Harga Satuan: Sedang (Rp1.64)
- Jumlah Pembelian: Tinggi (30.41 unit)
- Diskon: Tinggi (6.7%)
- Total per Transaksi: Rp45.13
- Total Penjualan: Rp34.614,21
- Jumlah Pelanggan Unik: 698
- Jumlah Pesanan: 767
- Produk Favorit: Fritz-Kola
- Kategori: Soft Drinks
- Tipe Pelanggan: B2B
- Wilayah: Mecklenburg-Vorpommern
Interpretasi:
Segmentasi bisnis yang membeli minuman ringan dalam jumlah besar dan menerima diskon substansial. Cocok dengan profil reseller, kafe, atau perusahaan yang rutin menyuplai minuman untuk konsumsi internal atau penjualan kembali.
Resume Cluster KMeans¶
Cluster | Customer Type | Produk Utama | Kategori Produk | Avg Unit Price | Avg Quantity | Avg Discount | Avg Total Price | Total Sales | Dominant Region | Karakteristik Utama |
---|---|---|---|---|---|---|---|---|---|---|
0 | B2B | Selters | Water | 0.83 | 31.93 | 0.065 | 25.60 | 13,822.83 | Hamburg | Pembelian grosir air mineral oleh bisnis; kuantitas tinggi, harga murah, diskon kecil. |
1 | B2C | Fritz-Kola | Soft Drinks | 1.86 | 8.01 | 0.000 | 15.16 | 37,353.56 | Bremen | Konsumen ritel membeli soft drinks dalam jumlah kecil; tanpa diskon, transaksi luas. |
2 | B2C | San Pellegrino | Water | 0.99 | 8.12 | 0.000 | 8.04 | 13,297.38 | Niedersachsen | Pembeli pribadi air mineral premium; transaksi kecil, tanpa diskon, preferensi kualitas. |
3 | B2B | Hohes C Orange | Juices | 2.33 | 29.20 | 0.069 | 62.46 | 32,293.65 | Nordrhein-Westfalen | Bisnis pembeli jus dalam jumlah besar; diskon relatif tinggi, nilai transaksi besar. |
4 | B2C | Mango Juice | Juices | 2.71 | 7.93 | 0.000 | 21.52 | 34,622.46 | Hamburg | Konsumen premium B2C membeli jus mahal; volume kecil, tanpa diskon, daya beli tinggi. |
5 | B2B | Fritz-Kola | Soft Drinks | 1.64 | 30.41 | 0.067 | 45.13 | 34,614.21 | Mecklenburg-Vorpommern | Pelanggan bisnis membeli soft drinks dalam volume besar dengan diskon signifikan. |
7. Mengeksport Data¶
💡
Untuk export dataset akan menggunakan label cluster hasil PCA karena mendapatkan Silhoute Score yang lebih baik (87.9%)
df_final.drop(columns=['Cluster']).to_csv('dataset-label.csv', index=False)