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¶

In [1]:
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¶

In [2]:
df = pd.read_csv('synthetic_beverage_sales_data.csv')
df.head()
Out[2]:
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)¶

In [3]:
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
In [4]:
df.head()
Out[4]:
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
In [5]:
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
In [6]:
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.

In [7]:
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¶

In [8]:
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¶

In [9]:
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.

In [10]:
df.describe()
Out[10]:
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 5,88,tapihargainicukupbervariasi,bahkanadayangsampai5,88,tapihargainicukupbervariasi,bahkanadayangsampai138,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.

In [11]:
sns.set(style='whitegrid')
plt.rcParams['figure.figsize'] = (15, 8)
In [12]:
# 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()
No description has been provided for this image

💡 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.

In [13]:
# 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()
No description has been provided for this image
No description has been provided for this image

💡

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.

In [14]:
# 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()
No description has been provided for this image

💡 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.

In [15]:
# 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()
No description has been provided for this image

💡 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.

In [16]:
# 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()
No description has been provided for this image

💡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¶

In [17]:
sns.pairplot(df, vars=df.select_dtypes(include=['float64', 'int64']))
plt.show()
No description has been provided for this image

5. Data Preprocessing¶

Penanganan Outlier¶

In [18]:
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
In [19]:
# 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
In [20]:
# 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()
No description has been provided for this image
In [21]:
df_cleaned = df_clean.copy()
In [22]:
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
In [23]:
df_cleaned = df_cleaned.sort_values(by='Order_ID', ascending=True).reset_index(drop=True)
df_cleaned
Out[23]:
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¶
In [24]:
# Fitur yang digunakan
categoric_cols = ['Category', 'Region', 'Customer_Type']
numeric_cols = ['Unit_Price', 'Quantity', 'Total_Price', 'Discount']
In [25]:
# Inisiasi Encoder dan Scaler
encoder = OneHotEncoder(sparse_output=False, handle_unknown='ignore')
scaler = MinMaxScaler()
In [26]:
# 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
)
In [27]:
In [28]:
df_combine.head()
Out[28]:
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¶

In [29]:
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_)
In [30]:
# 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()
No description has been provided for this image

💡 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.

In [31]:
# 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¶

In [32]:
silhouette_avg = silhouette_score(df_combine, kmeans.labels_)
print(f'Silhouette Score: {silhouette_avg}')
Silhouette Score: 0.37988763640929135

c. Feature Selection (Opsional)¶

In [33]:
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_)
In [34]:
# 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()
No description has been provided for this image

Hasil n_cluster optimal masih yang sama yaitu 6.

In [35]:
# 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¶

In [36]:
No description has been provided for this image
In [37]:
df_combine['Cluster_PCA'] = clusters
In [38]:
df_combine
Out[38]:
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¶

In [39]:
# invers data
df_num_normalize = df_combine[numeric_cols]
df_num_inverse = pd.DataFrame(scaler.inverse_transform(df_num_normalize), columns=numeric_cols)
In [40]:
In [41]:
In [42]:
# Tambah kembali kolom Cluster
df_final['Cluster'] = df_combine['Cluster'].values
df_final['Cluster_PCA'] = df_combine['Cluster_PCA'].values
In [43]:
df_final.head(2)
Out[43]:
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
In [44]:
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)
In [45]:
df_final.head(2)
Out[45]:
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
In [46]:
df_cleaned.head(2)
Out[46]:
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
In [47]:
df_final.tail(2)
Out[47]:
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
In [48]:
df_cleaned.tail(2)
Out[48]:
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)

In [49]:
df_final
Out[49]:
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

In [50]:
# 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")
In [51]:
agg_summary
Out[51]:
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%)

In [52]:
df_final.drop(columns=['Cluster']).to_csv('dataset-label.csv', index=False)