End-to-end data pipeline analyzing transaction data from the G Coffee chain in Malaysia (Kuala Lumpur, Selangor, Putrajaya) from July 2023 to June 2025. The pipeline encompasses data ingestion, validation, cleaning, integration, feature engineering, exploratory analysis, and forecasting — producing datasets ready for RFM segmentation, market basket analysis (Apriori), supervised learning (XGBoost), and hybrid time-series forecasting.
Capstone-Tempa/
├───dashboard/
│ ├───app.py # Dashboard Streamlit interaktif
│ └───Groq_analyst.py # AI Analyst berbasis Groq API
├───data/
│ ├───*.parquet # Data hasil pipeline (transactions, users, dll)
│ ├───*.csv # Association rules
│ ├───*.json # Cluster metadata
│ ├───*.joblib # Model ML (K-Means, scaler)
│ └───*.txt # Groq API key
├───function.py # Utility functions
├───01-LoadData.ipynb # Notebook pipeline utama
├───... (notebook lainnya)
├───README.md
├───requirements.txt # Dependencies Python
├───url.txt # URL dashboard
├───ARCHITECTURE.md
└───SourceOfTruth.md
| Aspect | Description |
|---|---|
| Objective | Clean, integrate, and engineer features from 7 raw CSV tables into master datasets for customer segmentation, basket analysis, and transaction forecasting. |
| Domain | Retail coffee shop chain — 10 outlets, 8 beverage items, >14.6M transactions. |
| Data Source | Kaggle — G Coffee Shop Transaction 202307 to 202506 |
| Time Span | 1 July 2023 – 30 June 2025 (2 years) |
| Volume | ~14.6M transactions, ~29.2M line items, ~2.2M registered users |
| Currency | Ringgit Malaysia (RM) — converted in-memory to IDR (1 RM = Rp 3,500) during EDA only. All parquet files store RM. |
- Downloads dataset from Kaggle via
kagglehub.dataset_download(). - Reads partitioned CSV files with
glob+pd.concat():transactions/*.csv(14,623,691 rows)transaction_items/*.csv(29,246,323 rows)users/*.csv(2,196,257 rows)menu_items.csv,stores.csv,vouchers.csv,payment_methods.csv(single files).
- Memory Optimisation via
function.py:optimize_numeric_data()— downcastsfloat64 -> float32,int64 -> int8/Int32.optimize_object_data()— converts low-cardinality (<5%) object columns tocategory.
- Result: Memory footprint reduced significantly, e.g., Transactions from 2,915 MB to 669 MB.
- Output: Intermediate
.parquetfiles per table.
- Data type standardisation:
transaction_id->struser_id,voucher_id->Int64(nullable)created_at,birthdate,registered_at->datetime64valid_from,valid_to->datetime64
- Deep validation (
run_deep_validation()):- PK duplicates: 0 duplicates found.
- FK integrity: 0 orphan transactions by
store_id. - Future dates: 0 transactions with
created_atafter today. - Age bounds: 0 users with age < 12 or > 100 at registration.
- Output: Validated
.parquetfiles per table.
- Orphan transactions (user_id not in Users table): 0 rows.
- Negative monetary values: 0 rows.
- Negative item quantity/price: 0 rows.
- Orphan items (transaction_id not in header): 0 rows.
- Unregistered menu items: 0 rows.
- Price consistency: 0 items with differing
unit_price.
- Full duplicates in
df_TransItem: 802,939 rows. - Composite duplicates
(transaction_id, item_id, created_at): 4,645,360 rows. - Policy: Remove all rows within a
(transaction_id, item_id)group with time difference <= 30 seconds — 2,360,635 rows removed. - Deterministic dedup (Rev): Row with maximum
subtotalretained. - Final: 0 duplicate rows remaining.
- 2,284,725 transactions had
original_amountinconsistent withSUM(subtotal)of items. - Correction:
original_amountoverwritten withSUM(subtotal). - Final:
final_amount = original_amount - discount_applied. - Rev:
original_amount_headerbacked up as audit trail before overwrite.
- Negative discount check: 784 transactions where discount >
original_amount(caused by SALES50 voucher). - Capping:
discount_applied = min(discount_applied, original_amount). Result: 0 negative transactions. - Cross-check against voucher rules: 0 mismatches.
Three methods compared for final_amount (initial skewness ~0.53):
| Method | Data Loss | Final Skewness |
|---|---|---|
| IQR (1.5xIQR) | 30,633 (0.21%) | 0.50 |
| Capping (P99) | 0 (0%) | 0.49 |
| Z-Score (|z|>3) | 30,619 (0.21%) | 0.50 |
Decision: Capping at percentile 99 selected — retains 100% of data with best skewness reduction.
available_fromandavailable_tocolumns in menu_items dropped (100% null).
Step 1: df_MasterTrans = df_Trans
LEFT JOIN df_Users -> ON user_id
LEFT JOIN df_stores -> ON store_id
Step 2: df_Master = df_TransItem
LEFT JOIN df_menu -> ON item_id
LEFT JOIN df_MasterTrans -> ON transaction_id
Step 3: df_Master
LEFT JOIN df_payment -> ON payment_method_id = method_id
- Row count: 26,885,688 (matches input).
- Orphan checks: 0 for menu, store, header.
- Financial audit: 14,623,691 transactions balanced (
original_amount = SUM(subtotal)). - Column cleanup: Redundant
_x/_ysuffix columns removed.
| Feature | Derivation |
|---|---|
hour |
dt.hour (0-23) |
month |
dt.month (1-12) |
day_name |
dt.day_name() |
month_name |
dt.month_name() |
is_weekend |
'Weekend' if Saturday/Sunday, else 'Weekday' |
transaction_period |
Morning (5-10), Afternoon (11-15), Evening (16-19), Night (20-23), Late Night (0-4) |
| Feature | Derivation |
|---|---|
member_status |
'Member' if user_id NOT NULL, else 'Guest' |
is_voucher_used |
'Voucher' if voucher_id NOT NULL, else 'No Voucher' |
discount_ratio |
discount_applied / (original_amount + 1e-6) |
Group by transaction_id:
| Column | Aggregation | New Name |
|---|---|---|
quantity |
SUM | basket_size |
final_amount |
MAX (deterministic) | -- |
discount_applied |
MAX | -- |
| Other fields | MAX | -- |
item_id |
COUNT DISTINCT | item_count (Rev) |
- Snapshot date:
MAX(created_at) + 1 day(2025-07-01). - Scope: Members only (
user_id NOT NULL). - Components: Recency (days since last transaction), Frequency (transaction count), Monetary (total spend).
- Scaled RFM (Rev): StandardScaler ->
RFM_Scaled_Recency,RFM_Scaled_Frequency,RFM_Scaled_Monetary.
- Binary matrix
transaction_id x item_name(1 = purchased, 0 = not). - Filters: remove items with support < 0.1% (none removed); remove single-item baskets (5.6M, 38%); remove baskets > 30 items (none).
- Result: 9,064,669 transactions x 8 items, sparsity 70.6%.
- Output:
df_basket_apriori.parquet.
- 80/20 temporal split:
- Training: 11,698,952 transactions (80%) -- 2023-07-01 to 2025-02-04.
- Test: 2,924,739 transactions (20%) -- 2025-02-04 to 2025-06-30.
- Output:
df_train.parquet,df_test.parquet.
| Metric | Value (RM) | Value (IDR x Rp3,500) |
|---|---|---|
| Total Revenue | ~RM 444.0 million | ~Rp 1.55 trillion |
| Total Transactions | 14,623,691 | -- |
| Average Transaction Value | ~RM 30.36 | ~Rp 106,260 |
| Member Revenue Share | ~50% | -- |
| Guest Revenue Share | ~50% | -- |
- Time-Based: Daily revenue trend, hourly traffic (peak vs off-peak), monthly revenue, period-based patterns.
- Customer: Revenue split (Member vs Guest), ATV comparison, repeat vs one-time behaviour, preference heatmap.
- Product: Top 10 menu items by quantity, coffee vs non-coffee revenue, preference by time period and city.
- Spatial: Revenue and ATV per city outlet.
- Payment: Revenue distribution across cash, card, ewallet methods.
- Promotion: Voucher impact on basket size, sensitivity analysis by Member vs Guest.
- RFM Segmentation (Rev): Heuristic quartile-based segments (Champions, Loyal, At Risk, Regular).
This notebook supersedes the legacy notebooks 07.ipynb, 08-Benchmark-Models.ipynb, 09_Hybrid_Forecast_HW_XGB.ipynb, and 09-testingXgb.ipynb. It consolidates all forecasting steps into a single pipeline with four sections:
- Univariate time-series models fitted per branch.
- ARIMA/SARIMA with automatic order selection via
auto_arima. - Prophet (Facebook) with weekly seasonality.
- Metrics: MAE, RMSE, MAPE per branch, aggregated across all 10 branches.
- Pooled multivariate model using autoregressive features (lag_1, lag_7, rolling_avg_7), calendar features (day_of_week, month), voucher_rate, and one-hot encoded city.
- Temporal train/test split at 2025-03-25 (80/20).
- Feature importance analysis (gain-based).
- Residual diagnostics.
Three architectures compared:
- HWR-XGB: Holt-Winters (additive trend, seasonal_periods=365) + XGBoost on residuals.
- SARIMA-XGB: SARIMA + XGBoost on residuals.
- Prophet-XGB: Prophet + XGBoost on residuals.
XGBoost on residuals uses only non-autoregressive features (day_of_week, month, voucher_rate) to avoid the low-pass filter problem in recursive forecasting.
- Cross-architecture comparison on held-out test set.
- Best model selection: lowest MAE on test set.
- 90-day ahead forecast saved as
df_forecast_90days.parquetfor the Voucher Engine. - Forecast variability check (min, max, std, range per branch).
| Method | Parameters | Returns | Description |
|---|---|---|---|
Duplicate(column_name) |
Column name | int | Counts duplicate rows in a column. |
BoxPlot(column_name, Target) |
Column, optional data | Visual | Boxplot for outlier detection. |
HistPlot(column_name, Target) |
Column, optional data | Visual | Histogram for distribution analysis. |
iqr(column_name) |
Column name | (outlier_count, original_count, cleaned_data) | IQR method: values outside 1.5xIQR considered outliers. |
capping(column_name) |
Column name | (skewness, affected_count, capped_data) | Winsorization at percentile 99. |
log_transform(column_name) |
Column name | (skewness, transformed_data) | Log1p transformation. |
z_score_method(column_name, threshold=3) |
Column, threshold | (outlier_count, original_count, cleaned_data) | Z-Score method: values with |z| > threshold are outliers. |
| Function | Parameters | Description |
|---|---|---|
optimize_numeric_data(df) |
DataFrame | Downcasts float64->float32, int64->int8/Int32. |
optimize_object_data(df, threshold=0.05) |
DataFrame, threshold | Converts low-cardinality (<5%) object columns to category. |
| Notebook | Functions Called |
|---|---|
01-LoadData |
optimize_numeric_data(), optimize_object_data() |
03-DataCleaning |
CleaningData (.Duplicate(), .BoxPlot(), .HistPlot(), .iqr(), .capping(), .z_score_method()) |
| File | Rows | Description |
|---|---|---|
transactions.parquet |
14,623,691 | Transaction headers -- after load and validation. |
transaction_items.parquet |
29,246,323 | Items per transaction -- after load and validation. |
transactions_capping.parquet |
14,623,691 | Headers with outliers capped at P99. |
transaction_items_cleaned.parquet |
26,885,688 | Items after duplicate removal. |
users_cleaned.parquet |
2,196,257 | Users after cleaning. |
menu_cleaned.parquet |
8 | Menu without available_from/to columns. |
stores_cleaned.parquet |
10 | Store data -- intact. |
df_Master_Final.parquet |
26,885,688 | Item-level master (all tables joined). |
df_Master_FE.parquet |
26,885,688 | Master with engineered features. |
df_transaction_features.parquet |
14,623,691 | Transaction-level features (1 row per transaction). |
df_rfm.parquet |
2,196,257 | RFM per user + scaled features. |
df_basket_apriori.parquet |
9,064,669 | Binary matrix for Apriori. |
df_train.parquet |
11,698,952 | Training set (80% temporal split). |
df_test.parquet |
2,924,739 | Test set (20% temporal split). |
df_forecast_90days.parquet |
900 (90 days x 10 branches) | 90-day forecast output (best model) for Voucher Engine. |
- Python 3.12+
- pandas, numpy, matplotlib, seaborn, scipy
- kagglehub (dataset download)
- scikit-learn (StandardScaler, K-Means)
- mlxtend (frequent_patterns Apriori)
- pyarrow (Parquet format)
- statsmodels (ExponentialSmoothing, ARIMA/SARIMA via pmdarima)
- prophet (Facebook Prophet)
- xgboost (gradient-boosted trees)
- pmdarima (auto_arima)
# Dari root folder Capstone-Tempa/
cd dashboard
streamlit run app.pyAtau langsung:
streamlit run dashboard/app.pyDashboard juga sudah dideploy dan bisa diakses di:
https://g-coffee-dashboard.streamlit.app/
Run notebooks in sequence:
01-LoadData.ipynb
02-DataValidation.ipynb
03-DataCleaning.ipynb (or 03-DataCleaning-Rev.ipynb)
04-JoinData.ipynb (or 04-JoinData-Rev.ipynb)
05-FeatureEngineering.ipynb (or 05-FeatureEngineering-Rev.ipynb)
06-EDA.ipynb (or 06-EDA-Rev.ipynb)
06_Modeling_and_Evaluation.ipynb (consolidated forecasting pipeline)
Secondary notebooks (run after 05-FeatureEngineering):
K-MeansMember.ipynb + aprioriMember.ipynb
K-MeansNonMember.ipynb + aprioriNonMember.ipynb
Legacy notebooks 07.ipynb, 08-Benchmark-Models.ipynb, 09_Hybrid_Forecast_HW_XGB.ipynb, and 09-testingXgb.ipynb are superseded by 06_Modeling_and_Evaluation.ipynb and retained for reference only.
Ensure function.py is in the same directory. Internet connectivity and Kaggle API credentials are required for 01-LoadData.ipynb.