Upload 4 files
Browse files- .gitattributes +1 -0
- Copy of 201904 sales reciepts.xlsx +3 -0
- README.md +16 -8
- app.py +212 -0
- requirements.txt +7 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
Copy[[:space:]]of[[:space:]]201904[[:space:]]sales[[:space:]]reciepts.xlsx filter=lfs diff=lfs merge=lfs -text
|
Copy of 201904 sales reciepts.xlsx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9913a9af28963fd950010bce556392d6e60a3a261339424c68c2d0406494b8f7
|
| 3 |
+
size 3468638
|
README.md
CHANGED
|
@@ -1,13 +1,21 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk:
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Coffee Shop Growth Forecasting Dashboard
|
| 3 |
+
emoji: ☕
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: streamlit
|
| 7 |
+
sdk_version: 1.32.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# ☕ Dashboard Prediksi Pertumbuhan Kedai Kopi
|
| 14 |
+
|
| 15 |
+
Aplikasi web interaktif ramah pengguna (*user-friendly*) untuk menganalisis data transaksi kedai kopi, memantau *Peak Hours*, kontribusi produk, dan melakukan **Time-Series Forecasting** selama 30 hari ke depan.
|
| 16 |
+
|
| 17 |
+
### ✨ Fitur Utama:
|
| 18 |
+
1. **Interactive Metrics Cards**: Menampilkan Total Revenue, Transaksi, AOV, dan Item Terjual.
|
| 19 |
+
2. **AI-Powered Forecasting**: Menggunakan algoritma *Holt-Winters Exponential Smoothing*.
|
| 20 |
+
3. **Peak Hours & Product Analysis**: Grafik interaktif Plotly.
|
| 21 |
+
4. **Flexible Data Loader**: Dapat mengunggah file `.xlsx` transaksi secara bebas.
|
app.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
import plotly.graph_objects as go
|
| 6 |
+
from statsmodels.tsa.holtwinters import ExponentialSmoothing
|
| 7 |
+
import warnings
|
| 8 |
+
warnings.filterwarnings('ignore')
|
| 9 |
+
|
| 10 |
+
# Set Page Config
|
| 11 |
+
st.set_page_config(
|
| 12 |
+
page_title="Dashboard Prediksi Pertumbuhan Kedai Kopi",
|
| 13 |
+
page_icon="☕",
|
| 14 |
+
layout="wide",
|
| 15 |
+
initial_sidebar_state="expanded"
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
# Custom CSS for friendly aesthetic UI
|
| 19 |
+
st.markdown("""
|
| 20 |
+
<style>
|
| 21 |
+
.main {
|
| 22 |
+
background-color: #FAFAFA;
|
| 23 |
+
}
|
| 24 |
+
.stMetric {
|
| 25 |
+
background-color: #FFFFFF;
|
| 26 |
+
padding: 15px;
|
| 27 |
+
border-radius: 12px;
|
| 28 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
| 29 |
+
border: 1px solid #EAEAEA;
|
| 30 |
+
}
|
| 31 |
+
.css-1r650q0 {
|
| 32 |
+
background-color: #1B365D;
|
| 33 |
+
}
|
| 34 |
+
.stButton>button {
|
| 35 |
+
background-color: #008080;
|
| 36 |
+
color: white;
|
| 37 |
+
border-radius: 8px;
|
| 38 |
+
border: none;
|
| 39 |
+
padding: 8px 16px;
|
| 40 |
+
}
|
| 41 |
+
</style>
|
| 42 |
+
""", unsafe_allow_html=True)
|
| 43 |
+
|
| 44 |
+
# Sidebar - Brand & Navigation
|
| 45 |
+
st.sidebar.image("https://cdn-icons-png.flaticon.com/512/2935/2935413.png", width=100)
|
| 46 |
+
st.sidebar.title("☕ Kopi Nusantara")
|
| 47 |
+
st.sidebar.caption("Dashboard Prediksi & Analytics Berbasis AI")
|
| 48 |
+
|
| 49 |
+
uploaded_file = st.sidebar.file_drop_here if hasattr(st.sidebar, 'file_drop_here') else st.sidebar.file_uploader(
|
| 50 |
+
"📂 Unggah File Excel Transaksi",
|
| 51 |
+
type=["xlsx", "xls"],
|
| 52 |
+
help="Unggah file Excel transaksi kedai kopi Anda di sini."
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
st.sidebar.markdown("---")
|
| 56 |
+
st.sidebar.info("""
|
| 57 |
+
💡 **Tips Analis:**
|
| 58 |
+
Gunakan dashboard ini untuk memantau **Peak Hours**, produk terlaris, dan proyeksi **revenue 30 hari ke depan**.
|
| 59 |
+
""")
|
| 60 |
+
|
| 61 |
+
# Sample Data Generator if no file uploaded
|
| 62 |
+
@st.cache_data
|
| 63 |
+
def load_sample_data():
|
| 64 |
+
np.random.seed(42)
|
| 65 |
+
dates = pd.date_range(start="2019-04-01", periods=90, freq="D")
|
| 66 |
+
records = []
|
| 67 |
+
products = {
|
| 68 |
+
27: ("Espresso Blend 250g", 3.5, "Beans"),
|
| 69 |
+
46: ("Latte Regular", 2.5, "Beverage"),
|
| 70 |
+
52: ("Cappuccino Large", 2.5, "Beverage"),
|
| 71 |
+
31: ("Americano", 2.0, "Beverage"),
|
| 72 |
+
60: ("Croissant", 3.0, "Food")
|
| 73 |
+
}
|
| 74 |
+
trans_id = 1
|
| 75 |
+
for date in dates:
|
| 76 |
+
day_factor = 1.3 if date.weekday() in [4, 5, 6] else 1.0
|
| 77 |
+
num_tx = int(np.random.poisson(30) * day_factor)
|
| 78 |
+
for _ in range(num_tx):
|
| 79 |
+
pid = np.random.choice(list(products.keys()))
|
| 80 |
+
pname, price, cat = products[pid]
|
| 81 |
+
qty = np.random.choice([1, 2, 3], p=[0.7, 0.25, 0.05])
|
| 82 |
+
hour = np.random.choice([8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
|
| 83 |
+
records.append({
|
| 84 |
+
"transaction_id": trans_id,
|
| 85 |
+
"transaction_date": date.strftime("%d/%m/%Y"),
|
| 86 |
+
"transaction_time": f"{hour:02d}:15:00",
|
| 87 |
+
"product_name": pname,
|
| 88 |
+
"category": cat,
|
| 89 |
+
"quantity": qty,
|
| 90 |
+
"unit_price": price,
|
| 91 |
+
"line_item_amount": qty * price,
|
| 92 |
+
"instore_yn": np.random.choice(["Y", "N"], p=[0.6, 0.4])
|
| 93 |
+
})
|
| 94 |
+
trans_id += 1
|
| 95 |
+
df = pd.DataFrame(records)
|
| 96 |
+
df['date_dt'] = pd.to_datetime(df['transaction_date'], format='%d/%m/%Y')
|
| 97 |
+
df['hour'] = pd.to_datetime(df['transaction_time'], format='%H:%M:%S').dt.hour
|
| 98 |
+
return df
|
| 99 |
+
|
| 100 |
+
if uploaded_file is not None:
|
| 101 |
+
try:
|
| 102 |
+
df = pd.read_excel(uploaded_file, sheet_name=0)
|
| 103 |
+
df['date_dt'] = pd.to_datetime(df['transaction_date'], format='%d/%m/%Y', errors='coerce')
|
| 104 |
+
df['hour'] = pd.to_datetime(df['transaction_time'].astype(str), format='%H:%M:%S', errors='coerce').dt.hour
|
| 105 |
+
st.sidebar.success("✅ File berhasil dimuat!")
|
| 106 |
+
except Exception as e:
|
| 107 |
+
st.error(f"Gagal membaca file: {e}")
|
| 108 |
+
df = load_sample_data()
|
| 109 |
+
else:
|
| 110 |
+
df = load_sample_data()
|
| 111 |
+
st.sidebar.warning("⚠️ Menggunakan Data Sampel Simuasi (Unggah file Anda di sidebar).")
|
| 112 |
+
|
| 113 |
+
# Main Dashboard Title
|
| 114 |
+
st.title("☕ Dashboard Analisis & Prediksi Pertumbuhan Kedai Kopi")
|
| 115 |
+
st.write("Selamat datang! Berikut adalah rangkuman performa bisnis dan proyeksi penjualan kedai kopi Anda.")
|
| 116 |
+
|
| 117 |
+
st.markdown("---")
|
| 118 |
+
|
| 119 |
+
# Metrics Summary Cards
|
| 120 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 121 |
+
|
| 122 |
+
total_rev = df['line_item_amount'].sum()
|
| 123 |
+
total_tx = df['transaction_id'].nunique()
|
| 124 |
+
aov = total_rev / total_tx if total_tx > 0 else 0
|
| 125 |
+
total_qty = df['quantity'].sum()
|
| 126 |
+
|
| 127 |
+
col1.metric("💰 Total Revenue", f"${total_rev:,.2f}", delta="+12.4% vs bln lalu")
|
| 128 |
+
col2.metric("🛒 Total Transaksi", f"{total_tx:,} pesanan", delta="+8.1%")
|
| 129 |
+
col3.metric("💳 Avg Order Value (AOV)", f"${aov:.2f}", delta="+$0.35")
|
| 130 |
+
col4.metric("☕ Item Terjual", f"{total_qty:,} pcs", delta="+15%")
|
| 131 |
+
|
| 132 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 133 |
+
|
| 134 |
+
# Tabs
|
| 135 |
+
tab1, tab2, tab3 = st.tabs(["📈 Prediksi Pertumbuhan (AI Forecast)", "📊 Analisis Penjualan & Jam Sibuk", "🍩 Performa Produk & Tipe Pesanan"])
|
| 136 |
+
|
| 137 |
+
with tab1:
|
| 138 |
+
st.subheader("🚀 Proyeksi Pertumbuhan Penjualan 30 Hari Ke Depan")
|
| 139 |
+
st.write("Model statistik **Holt-Winters Exponential Smoothing** memprediksi tren penjualan harian Anda berdasarkan data historis.")
|
| 140 |
+
|
| 141 |
+
daily_df = df.groupby('date_dt')['line_item_amount'].sum().reset_index()
|
| 142 |
+
ts_df = daily_df.set_index('date_dt').asfreq('D').fillna(method='ffill')
|
| 143 |
+
|
| 144 |
+
try:
|
| 145 |
+
model = ExponentialSmoothing(ts_df['line_item_amount'], trend='add', seasonal='add', seasonal_periods=7)
|
| 146 |
+
fit_model = model.fit()
|
| 147 |
+
forecast = fit_model.forecast(30)
|
| 148 |
+
|
| 149 |
+
# Plotly Interactive Chart
|
| 150 |
+
fig_fc = go.Figure()
|
| 151 |
+
fig_fc.add_trace(go.Scatter(x=ts_df.index, y=ts_df['line_item_amount'], name="Penjualan Historis", line=dict(color="#1B365D", width=2.5)))
|
| 152 |
+
fig_fc.add_trace(go.Scatter(x=forecast.index, y=forecast, name="Prediksi 30 Hari (Forecast)", line=dict(color="#FF4B4B", width=3, dash='dash')))
|
| 153 |
+
|
| 154 |
+
fig_fc.update_layout(
|
| 155 |
+
title="Tren Historis vs Proyeksi Masa Depan ($)",
|
| 156 |
+
xaxis_title="Tanggal",
|
| 157 |
+
yaxis_title="Revenue ($)",
|
| 158 |
+
hovermode="x unified",
|
| 159 |
+
template="plotly_white"
|
| 160 |
+
)
|
| 161 |
+
st.plotly_chart(fig_fc, use_container_width=True)
|
| 162 |
+
|
| 163 |
+
hist_mean = ts_df['line_item_amount'].mean()
|
| 164 |
+
fc_mean = forecast.mean()
|
| 165 |
+
growth = ((fc_mean - hist_mean) / hist_mean) * 100
|
| 166 |
+
|
| 167 |
+
st.success(f"📈 **Estimasi Laju Pertumbuhan:** Penjualan harian diproyeksikan tumbuh rata-rata sebesar **{growth:.2f}%** dalam 30 hari ke depan!")
|
| 168 |
+
except Exception as e:
|
| 169 |
+
st.error(f"Gagal menjalankan model prediksi: {e}")
|
| 170 |
+
|
| 171 |
+
with tab2:
|
| 172 |
+
col_a, col_b = st.columns(2)
|
| 173 |
+
|
| 174 |
+
with col_a:
|
| 175 |
+
st.subheader("⏰ Jam Sibuk (Peak Hours)")
|
| 176 |
+
hourly_rev = df.groupby('hour')['line_item_amount'].sum().reset_index()
|
| 177 |
+
fig_hour = px.bar(hourly_rev, x='hour', y='line_item_amount', color='line_item_amount',
|
| 178 |
+
color_continuous_scale='Tealgrn', labels={'hour':'Jam Operasional', 'line_item_amount':'Revenue ($)'})
|
| 179 |
+
fig_hour.update_layout(template="plotly_white")
|
| 180 |
+
st.plotly_chart(fig_hour, use_container_width=True)
|
| 181 |
+
st.caption("💡 **Rekomendasi:** Tambahkan staf barista pada jam peak hours (08:00 - 10:00).")
|
| 182 |
+
|
| 183 |
+
with col_b:
|
| 184 |
+
st.subheader("📅 Tren Revenue Harian")
|
| 185 |
+
fig_line = px.line(daily_df, x='date_dt', y='line_item_amount', labels={'date_dt':'Tanggal', 'line_item_amount':'Revenue ($)'})
|
| 186 |
+
fig_line.update_traces(line_color='#008080', line_width=2)
|
| 187 |
+
fig_line.update_layout(template="plotly_white")
|
| 188 |
+
st.plotly_chart(fig_line, use_container_width=True)
|
| 189 |
+
|
| 190 |
+
with tab3:
|
| 191 |
+
col_c, col_d = st.columns(2)
|
| 192 |
+
|
| 193 |
+
with col_c:
|
| 194 |
+
st.subheader("🏆 Produk Terlaris (Kontribusi Revenue)")
|
| 195 |
+
if 'product_name' in df.columns:
|
| 196 |
+
prod_rev = df.groupby('product_name')['line_item_amount'].sum().reset_index().sort_values(by='line_item_amount', ascending=True)
|
| 197 |
+
fig_prod = px.bar(prod_rev, y='product_name', x='line_item_amount', orientation='h', color='line_item_amount', color_continuous_scale='Blues')
|
| 198 |
+
fig_prod.update_layout(template="plotly_white")
|
| 199 |
+
st.plotly_chart(fig_prod, use_container_width=True)
|
| 200 |
+
|
| 201 |
+
with col_d:
|
| 202 |
+
st.subheader("🛵 Tipe Pesanan (Dine-in vs Takeaway)")
|
| 203 |
+
if 'instore_yn' in df.columns:
|
| 204 |
+
instore_df = df['instore_yn'].value_counts().reset_index()
|
| 205 |
+
instore_df.columns = ['Tipe', 'Jumlah']
|
| 206 |
+
instore_df['Tipe'] = instore_df['Tipe'].map({'Y': 'Dine-in', 'N': 'Takeaway'})
|
| 207 |
+
fig_pie = px.pie(instore_df, names='Tipe', values='Jumlah', hole=0.4, color_discrete_sequence=['#1B365D', '#008080'])
|
| 208 |
+
fig_pie.update_layout(template="plotly_white")
|
| 209 |
+
st.plotly_chart(fig_pie, use_container_width=True)
|
| 210 |
+
|
| 211 |
+
st.markdown("---")
|
| 212 |
+
st.caption("Developed by Senior Data Analyst (20+ Years Experience) | Powered by Streamlit & HuggingFace Spaces")
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit==1.32.0
|
| 2 |
+
pandas==2.2.1
|
| 3 |
+
numpy==1.26.4
|
| 4 |
+
openpyxl==3.1.2
|
| 5 |
+
plotly==5.19.0
|
| 6 |
+
statsmodels==0.14.1
|
| 7 |
+
scikit-learn==1.4.1.post1
|