Skip to content

Latest commit

Β 

History

History
461 lines (387 loc) Β· 22 KB

File metadata and controls

461 lines (387 loc) Β· 22 KB

Architecture & Integration Skeleton

Customer Segmentation + Association Rules + Forecasting Pipeline

1. System Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                              STREAMLIT APP (app.py)                                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”‚
β”‚  β”‚      MEMBER TAB          β”‚  β”‚      NON-MEMBER (GUEST) TAB           β”‚                  β”‚
β”‚  β”‚  Β· Segment Distribution  β”‚  β”‚  Β· Segment Distribution               β”‚                  β”‚
β”‚  β”‚  Β· RFM Profiles + Radar  β”‚  β”‚  Β· Feature Profiles                   β”‚                  β”‚
β”‚  β”‚  Β· Rules per Segment     β”‚  β”‚  Β· Rules per Segment                  β”‚                  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚                             β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚  MEMBER MODELS     β”‚       β”‚   GUEST MODELS      β”‚
    β”‚  (member_*.joblib, β”‚       β”‚   (guest_*.joblib,  β”‚
    β”‚   *_member.*)      β”‚       β”‚   *_guest.*)         β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚                             β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚              PROCESSING LAYER                     β”‚
    β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
    β”‚  β”‚ K-MeansMember    β”‚  β”‚ K-MeansNonMember      β”‚  β”‚
    β”‚  β”‚ (RFM Clustering) β”‚  β”‚ (Transaction Feat.)   β”‚  β”‚
    β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
    β”‚           β”‚                       β”‚               β”‚
    β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
    β”‚  β”‚       aprioriMember / aprioriNonMember     β”‚  β”‚
    β”‚  β”‚       (Per-Segment Association Rules)       β”‚  β”‚
    β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
    β”‚                                                  β”‚
    β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
    β”‚  β”‚      06_Modeling_and_Evaluation.ipynb      β”‚  β”‚
    β”‚  β”‚  Β· Baseline (ARIMA/SARIMA/Prophet)         β”‚  β”‚
    β”‚  β”‚  Β· XGBoost Supervised Learning             β”‚  β”‚
    β”‚  β”‚  Β· Hybrid Detrending (HWR/SARIMA/Prophet)  β”‚  β”‚
    β”‚  β”‚  Β· Comparative Evaluation                  β”‚  β”‚
    β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚                 DATA LAYER                        β”‚
    β”‚  Β· df_transaction_features.parquet (14.6M rows)  β”‚
    β”‚  Β· df_basket_apriori.parquet (9.1M rows, 8 prods)β”‚
    β”‚  Β· df_rfm-Member.parquet (2.2M rows)             β”‚
    β”‚  Β· df_forecast_90days.parquet (900 rows)         β”‚
    β”‚                                                  β”‚
    β”‚                    ↓                              β”‚
    β”‚            VOUCHER ENGINE                        β”‚
    β”‚         (S_time, S_margin)                       β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2. Data Layer (Source Files)

File Rows Columns Content
df_transaction_features.parquet 14,623,691 17 All transactions (member + guest) with features
df_basket_apriori.parquet 9,064,669 9 One-hot encoded 8 beverage products per transaction_id
df_rfm-Member.parquet 2,196,257 5 RFM per member (Recency, Frequency, Monetary, user_id, is_repeat_customer)
df_forecast_90days.parquet 900 3 90-day forecast per branch (best model output)

Note: df_transaction_features.parquet uses categorical dtypes that cause pandas read_parquet() to fail. Must read with PyArrow (pq.read_table()).


3. Processing Layer (Notebooks)

3a. K-MeansMember.ipynb β€” Member RFM Clustering

Input:  df_rfm-Member.parquet
        └─ columns: [user_id, Recency, Frequency, Monetary, is_repeat_customer]

Pipeline:
  1. Load RFM data (pandas read_parquet β€” aman, no categorical issue)
  2. Feature Transforms (7 methods):
       - No Transform (raw)
       - Log1p
       - Log1p β†’ StandardScaler
       - Log1p β†’ MinMaxScaler
       - QuantileTransformer (n_quantiles=1000, output_distribution='normal')
       - PowerTransformer (Yeo-Johnson)
       - PowerTransformer (Box-Cox after shift)
  3. MiniBatchKMeans (batch_size=8192, n_init=3, random_state=42)
     β†’ Loop k=2..10, pick best via Silhouette + DBI
  4. PCA (2D) for visualization
  5. HDBSCAN for label refinement
  6. Segment naming (manual mapping)
  7. Profile aggregation (R/F/M means per cluster)

Output:
  β”œβ”€β”€ model_kmeans_member.joblib     (MiniBatchKMeans object)
  β”œβ”€β”€ scaler_member.joblib           (best quantile scaler)
  β”œβ”€β”€ member_cluster_metadata.json   (segment labels, profiles, k, silhouette)
  └── df_member_with_segments.parquet (user_id + RFM + cluster + segment_name)
        └─ columns: [user_id, Recency, Frequency, Monetary, is_repeat_customer, cluster, segment_name]

3b. K-MeansNonMember.ipynb β€” Guest Transaction Clustering

Input:  df_transaction_features.parquet
        └─ filter: user_id.isna() β†’ ~7.3M guest transactions
        └─ features: [final_amount, basket_size, item_count, hour,
                      is_weekend_bool, is_voucher_used_bool, discount_ratio]

Pipeline: (same structure as Member, different features)
  1. Load via PyArrow (avoid categorical crash)
  2. Filter guest (user_id IS NULL)
  3. Feature transforms (7 methods)
  4. MiniBatchKMeans (k determined via elbow)
  5. PCA + HDBSCAN
  6. Segment naming: Big Spender, Weekend Visitor, Deal Hunter, Quick Buy
  7. Save model + scaler + metadata

Output:
  β”œβ”€β”€ model_kmeans_guest.joblib       (MiniBatchKMeans object)
  β”œβ”€β”€ scaler_guest.joblib             (best scaler)
  β”œβ”€β”€ guest_cluster_metadata.json     (segment labels, cluster_cols, optimal_k)
  └── df_guest_with_segments.parquet  (transaction_id + features + cluster + segment_name)
        └─ columns: [transaction_id, ..., cluster, cluster_normalized, segment_name]

3c. aprioriMember.ipynb β€” Member Association Rules (per Segment)

Input:
  β”œβ”€β”€ df_basket_apriori.parquet       (9M basket-product rows)
  β”œβ”€β”€ df_transaction_features.parquet (for user_id + member_status join)
  └── df_member_with_segments.parquet (for segment_name join via user_id)

Join Chain:
  basket_table ──(transaction_id)──→ tx_table ──(user_id)──→ seg_table
                                           β”‚
                                    filter member_status='Member'

Apriori:
  For each segment:
    df_seg = filter(segment_name == seg)
    frequent_itemsets = apriori(df_seg, min_support=0.01, use_colnames=True)
    rules = association_rules(frequent_itemsets, metric='confidence', min_threshold=0.1)
    rules['segment_name'] = seg

Output:
  β”œβ”€β”€ df_rules_member.parquet   (224 rules, 4 segments, 15 columns)
  └── df_rules_member.csv       (same, CSV)

Note: Lift < 1.0 (~0.70) for all rules β€” beverages are bought singly.
      Using confidence metric to capture all 56 pairwise rules per segment.

3d. aprioriNonMember.ipynb β€” Guest Association Rules (per Segment)

Input:
  β”œβ”€β”€ df_basket_apriori.parquet          (9M basket-product rows)
  β”œβ”€β”€ df_transaction_features.parquet    (for member_status join)
  └── df_guest_with_segments.parquet     (for segment_name join)

Join Chain:
  basket_table ──(transaction_id)──→ tx_table ──(transaction_id)──→ gseg_table
                                           β”‚
                                    filter member_status='Guest'

Apriori:
  For each segment:
    df_seg = filter(segment_name == seg)
    frequent_itemsets = apriori(df_seg, min_support=0.01, use_colnames=True)
    rules = association_rules(frequent_itemsets, metric='confidence', min_threshold=0.1)

Output:
  β”œβ”€β”€ df_rules_guest.parquet   (168 rules, 3 segments, 15 columns)
  └── df_rules_guest.csv       (same, CSV)

Note: Guest has 4 segments but Apriori produces rules for only 3
      (Big Spender, Weekend Visitor, Deal Hunter).
      "Quick Buy" has no beverage items in baskets β†’ no rules.

3e. 06_Modeling_and_Evaluation.ipynb β€” Forecasting Pipeline (Consolidated)

Input:  df_transaction_features.parquet
        └─ aggregated daily per city:
           [total_transactions, total_revenue, avg_basket,
            voucher_rate, lag_1, lag_7, rolling_avg_7,
            day_of_week, month, created_at, city]

Pipeline (4 sections):
  A. BASELINE MODELS (univariate, per branch)
     1. ARIMA - auto_arima(seasonal=False, stepwise=True)
     2. SARIMA - auto_arima(seasonal=True, m=7)
     3. Prophet - weekly_seasonality=True
     Metrics: MAE, RMSE, MAPE per branch; aggregate across 10 branches

  B. XGBoost SUPERVISED (pooled, multivariate)
     Features: [lag_1, lag_7, rolling_avg_7, day_of_week,
                month, voucher_rate, city_onehot]
     Train/test split: 2025-03-25 (80/20 temporal)
     Feature importance (gain-based)
     Residual diagnostics

  C. HYBRID DETRENDING ARCHITECTURES
     1. Holt-Winters (additive, seasonal_periods=365)
        └─ XGBoost on residuals [day_of_week, month, voucher_rate]
     2. SARIMA (1,0,1)(1,0,1,7)
        └─ XGBoost on residuals (same features)
     3. Prophet (weekly)
        └─ XGBoost on residuals (same features)
     Rationale: Non-autoregressive residual features avoid
                low-pass recursive forecast collapse

  D. EVALUATION AND OUTPUT
     Cross-architecture comparison (MAE, RMSE, R2, MAPE)
     Best model selection -> 90-day forecast
     Variability check (std/mean per branch)

Output:
  └── df_forecast_90days.parquet
        └─ columns: [branch, created_at, total_transactions]

Consumption:
  -> Voucher Engine (S_time, S_margin dynamic scoring)

Key design decision: XGBoost on residuals uses only day_of_week, month, and voucher_rate β€” autoregressive features (lag_1, rolling_avg_7) are excluded to prevent error accumulation in multi-step recursive forecasting.

Supersedes: Legacy notebooks 07.ipynb, 08-Benchmark-Models.ipynb, 09_Hybrid_Forecast_HW_XGB.ipynb, and 09-testingXgb.ipynb. These remain on disk but are no longer part of the active pipeline.


4. Model Layer (Saved Artifacts)

4a. Naming Convention

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Artifact              β”‚ Member Prefix        β”‚ Guest Prefix         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ KMeans model          β”‚ model_kmeans_member  β”‚ model_kmeans_guest   β”‚
β”‚ Scaler/Transform      β”‚ scaler_member        β”‚ scaler_guest         β”‚
β”‚ Cluster metadata      β”‚ member_cluster_meta  β”‚ guest_cluster_meta   β”‚
β”‚ Segment data          β”‚ df_member_with_seg.. β”‚ df_guest_with_seg..  β”‚
β”‚ Association rules     β”‚ df_rules_member      β”‚ df_rules_guest       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

4b. Metadata Contracts

member_cluster_metadata.json

{
  "model_type": "MiniBatchKMeans",
  "k": 2,
  "features": ["Recency", "Frequency", "Monetary"],
  "best_transform": "Quantile",
  "silhouette_score": 0.454,
  "total_members": 2196257,
  "cluster_labels": {
    "0": "At Risk Regulars",
    "1": "New Occasional",
    "2": "Hibernating",
    "3": "Champions"
  },
  "cluster_profiles": [
    {
      "cluster": 0,
      "count": 399069,
      "pct": 18.17,
      "revenue_share_pct": 44.0,
      "R_mean": 95.7, "F_mean": 7.8, "M_mean": 249.6,
      "R_median": 82.0, "F_median": 7.0, "M_median": 224.5,
      "monetary_sum": 99625920.0
    }
  ]
}

guest_cluster_metadata.json

{
  "model_type": "MiniBatchKMeans",
  "optimal_k": 4,
  "cluster_cols": [
    "final_amount", "basket_size", "item_count", "hour",
    "is_weekend_bool", "is_voucher_used_bool", "discount_ratio"
  ],
  "best_transform": "Quantile",
  "cluster_id_to_name": {
    "0": "Big Spender",
    "1": "Weekend Visitor",
    "2": "Deal Hunter",
    "3": "Quick Buy"
  }
}

5. Application Layer (app.py)

5a. Component Architecture

app.py
β”œβ”€β”€ @st.cache_data
β”‚   β”œβ”€β”€ load_json(path)         β†’ dict
β”‚   β”œβ”€β”€ load_rules(path)        β†’ pd.DataFrame
β”‚   └── load_segment_counts(path, col) β†’ pd.DataFrame
β”‚
β”œβ”€β”€ Sidebar
β”‚   └── radio("Select View")
β”‚       β”œβ”€β”€ "Member"
β”‚       └── "Non-Member (Guest)"
β”‚
β”œβ”€β”€ Member Tab
β”‚   β”œβ”€β”€ Metrics (total, k, silhouette, transform)
β”‚   β”œβ”€β”€ Segment Distribution (pie chart + table)
β”‚   β”œβ”€β”€ Segment Profiles (RFM means table)
β”‚   β”œβ”€β”€ RFM Radar (normalized, per segment)
β”‚   β”œβ”€β”€ Association Rules
β”‚   β”‚   β”œβ”€β”€ Segment selector (dropdown)
β”‚   β”‚   β”œβ”€β”€ Filter: min_lift, min_confidence (sliders)
β”‚   β”‚   β”œβ”€β”€ Rules table (sortable)
β”‚   β”‚   └── Top 15 lift bar chart
β”‚   └── Model Info (expandable JSON)
β”‚
└── Guest Tab
    β”œβ”€β”€ Metrics (total, k, model)
    β”œβ”€β”€ Segment Distribution (pie chart + table)
    β”œβ”€β”€ Cluster Features (list + segment names)
    β”œβ”€β”€ Association Rules (same structure as Member)
    β”‚   └── Handles empty rules gracefully
    └── Model Info (expandable JSON)

5b. Data Loading Strategy

Component Source Method
Segment counts *_with_segments.parquet pq.read_table(columns=['segment_name']) + Counter
Segment profiles *_cluster_metadata.json Direct json.load
Association rules df_rules_*.parquet pd.read_parquet (small data, <300 rows)
Model info *_cluster_metadata.json json.load
Visualizations Computed from above Plotly charts

Performance: load_segment_counts() reads only 1 column from multi-million-row parquet β†’ memory efficient.


6. Data Flow Diagram

Source                              Processing                          Output
────────────────────────────────────────────────────────────────────────────────────

df_rfm-Member.parquet ──→ K-MeansMember.ipynb ──→ model_kmeans_member.joblib
                                                     scaler_member.joblib
                                                     member_cluster_metadata.json
                                                     df_member_with_segments.parquet
                                                                β”‚
df_basket_apriori.parquet ──→ aprioriMember.ipynb ──────────────
df_transaction_features.parquet                                β”‚
                                                                β”œβ”€β”€β†’ app.py (Member tab)
                                                                β”‚
df_rules_member.parquet β†β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜


df_transaction_features.parquet ──→ K-MeansNonMember.ipynb ──→ model_kmeans_guest.joblib
(fitur guest, user_id IS NULL)                                    scaler_guest.joblib
                                                                    guest_cluster_metadata.json
                                                                    df_guest_with_segments.parquet
                                                                               β”‚
df_basket_apriori.parquet ──→ aprioriNonMember.ipynb ─────────────────────────
df_transaction_features.parquet                                               β”‚
                                                                               β”œβ”€β”€β†’ app.py (Guest tab)
                                                                               β”‚
df_rules_guest.parquet β†β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜


df_transaction_features.parquet ──→ 06_Modeling_and_Evaluation.ipynb ──→ df_forecast_90days.parquet
(daily aggregation)                                                             β”‚
                                                                                β”œβ”€β”€β†’ Voucher Engine
                                                                                β”‚   (S_time, S_margin)
                                                                                β”‚
                                                                          model artifacts:
                                                                          β”œβ”€β”€ hw_models (dict of ExponentialSmoothing)
                                                                          β”œβ”€β”€ xgb_model (XGBRegressor)
                                                                          β”œβ”€β”€ xgb_residual (XGBRegressor)
                                                                          β”œβ”€β”€ sar_macro_models (dict of ARIMA)
                                                                          └── prop_macro_models (dict of Prophet)

7. Integration Contracts

7a. Join Keys

Notebook Left Table Right Table Join Key Type
aprioriMember basket_table tx_table transaction_id inner
aprioriMember member_tbl seg_table user_id left outer
aprioriNonMember basket_table tx_table transaction_id inner
aprioriNonMember guest_tbl gseg_table transaction_id left outer

7b. Column Contracts

df_rules_member.parquet / df_rules_guest.parquet

antecedents        object   (set converted to comma-separated string)
consequents        object   (set converted to comma-separated string)
support            float64  (P(A βˆͺ B) / total)
confidence         float64  (P(B | A))
lift               float64  (confidence / P(B))
leverage           float64
conviction         float64
zhangs_metric      float64
jaccard            float64
certainty          float64
kulczynski         float64
representativity   float64
antecedent support float64  (P(A))
consequent support float64  (P(B))
segment_name       object   (segment identifier)

7c. Critical Rules

  1. Always use PyArrow when reading df_transaction_features.parquet (categorical dtype crash).
  2. Never remove segment_name from rules DataFrames β€” it's the primary join key for the app.
  3. All lift ~0.70 β€” beverages are rarely co-purchased. Use metric='confidence' with low threshold.
  4. Guest "Quick Buy" segment may have no Apriori rules β€” app must handle empty gracefully.

8. Extensibility

Adding a New Segment Type

  1. Create new K-Means notebook β†’ follow K-Means*.ipynb template
  2. New parquet output: df_<type>_with_segments.parquet
  3. New Apriori notebook β†’ copy structure, adjust join key
  4. New rules file: df_rules_<type>.parquet
  5. Add new tab in app.py following existing pattern

Adding New Products to Basket

  1. Add int8 flag column to df_basket_apriori.parquet
  2. No code changes needed β€” notebooks auto-detect product columns

Updating Segment Labels

  1. Edit the segment mapping in K-Means notebook
  2. Re-run notebook β†’ updates metadata JSON + segment parquet
  3. Re-run Apriori β†’ updates rules with new segment names
  4. App reads dynamically β€” no code changes needed