ZakyF commited on
Commit
fc91167
·
1 Parent(s): 993acf3

feat: Implement adaptive Next Best Offer (NBO) decision engine, enhance customer analysis with discretionary spending, and refine UI styling.

Browse files
Files changed (1) hide show
  1. app.py +104 -43
app.py CHANGED
@@ -14,15 +14,51 @@ if GOOGLE_API_KEY:
14
  except:
15
  client_ai = None
16
 
17
- # --- UI STYLE: BANK NAGARI LIGHT BLUE ---
18
  custom_css = """
19
  @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap');
20
  body, .gradio-container { font-family: 'Plus Jakarta Sans', sans-serif !important; background-color: #FFFFFF !important; }
21
- .nagari-header { background: linear-gradient(135deg, #0514DE 0%, #82C3EB 100%); padding: 35px; border-radius: 15px; border-bottom: 6px solid #F7BD87; margin-bottom: 25px; text-align: center; }
22
- .nagari-header h1 { color: #FFFFFF !important; font-weight: 800 !important; margin: 0; }
23
- .report-card { background: #FFFFFF; border-radius: 15px; padding: 25px; border: 1.5px solid #E0EDF4; box-shadow: 0 4px 15px rgba(5, 20, 222, 0.05); }
24
- .metric-box { background: #E0EDF4; padding: 20px; border-radius: 12px; border-left: 8px solid #0514DE; }
25
- .advice-section { background: #fffdf0; border: 1px solid #F7BD87; padding: 20px; border-radius: 10px; margin-top: 15px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  """
27
 
28
  class ArchonMasterEngine:
@@ -30,9 +66,9 @@ class ArchonMasterEngine:
30
  self.load_data()
31
 
32
  def load_data(self):
33
- # Proteksi data untuk ID yang bermasalah (seperti C0014)
34
  try:
35
- self.df_txn = pd.read_csv('transactions.csv', parse_dates=['date']).fillna("Umum")
 
36
  self.df_cust = pd.read_csv('customers.csv').fillna(0)
37
  self.df_bal = pd.read_csv('balances_revised.csv', parse_dates=['month']).fillna(0)
38
  self.df_rep = pd.read_csv('repayments_revised.csv', parse_dates=['due_date']).fillna("on_time")
@@ -40,7 +76,9 @@ class ArchonMasterEngine:
40
  print(f"File Error: {e}")
41
 
42
  def analyze(self, customer_id):
 
43
  cid = str(customer_id).strip().upper()
 
44
  u_txn = self.df_txn[self.df_txn['customer_id'] == cid].copy()
45
  u_bal = self.df_bal[self.df_bal['customer_id'] == cid].sort_values('month')
46
  u_rep = self.df_rep[self.df_rep['customer_id'] == cid]
@@ -56,6 +94,12 @@ class ArchonMasterEngine:
56
  expense = u_txn[u_txn['transaction_type'] == 'debit']['amount'].sum()
57
  er = min(expense / ref_income, 1.0) if ref_income > 0 else 1.0
58
 
 
 
 
 
 
 
59
  # Scoring logic (Fase 4 - Bobot 30/20/20/20/10)
60
  er_s = 1.0 if er > 0.8 else (0.5 if er > 0.5 else 0.0)
61
  bt_s = 1.0 if len(u_bal) >= 2 and u_bal.iloc[-1]['avg_balance'] < u_bal.iloc[-2]['avg_balance'] else 0.0
@@ -65,43 +109,60 @@ class ArchonMasterEngine:
65
  score = (0.3 * er_s) + (0.2 * bt_s) + (0.2 * od_s) + (0.2 * mp_s) + 0.1
66
  risk_lv = "HIGH" if score >= 0.7 else ("MEDIUM" if score >= 0.4 else "LOW")
67
 
68
- return risk_lv, score, er, u_bal, u_txn, expense, ref_income
69
 
70
- def build_expert_advice(self, risk_lv, score, er, u_bal, expense, income, u_txn):
71
- # ADAPTIVE DETERMINISTIC ADVISOR (Backup & Logic Engine)
72
- # Menghasilkan saran berdasarkan data riil nasabah secara otomatis
73
- msg = f"### 📊 ANALISIS INTELIJEN ARCHON\n"
74
  msg += f"Berdasarkan profil mutasi, tingkat resiliensi Bapak/Ibu berada di level **{risk_lv}** (Skor: {score:.2f}).\n\n"
75
 
76
- # Logika Rasio
77
- if er > 0.8:
78
- msg += f"🔴 **Peringatan Rasio**: Pengeluaran Anda (Rp{expense:,.0f}) sudah menyerap {er:.1%} dari pendapatan. Ini sangat kritis dan dapat mengganggu cicilan di masa depan. "
79
- elif er > 0.5:
80
- msg += f"🟡 **Evaluasi Belanja**: Rasio pengeluaran {er:.1%} mulai memasuki zona waspada. Disarankan melakukan efisiensi pada pos belanja non-esensial. "
81
- else:
82
- msg += f"🟢 **Kesehatan Arus Kas**: Rasio pengeluaran {er:.1%} sangat ideal. Bapak/Ibu memiliki surplus dana yang kuat untuk investasi. "
83
-
84
- # Logika Saldo
85
  if not u_bal.empty:
86
  last_bal = u_bal.iloc[-1]['avg_balance']
87
- if len(u_bal) > 1 and last_bal < u_bal.iloc[-2]['avg_balance']:
88
- msg += f"\n\n📉 **Tren Saldo**: Terjadi penurunan saldo rata-rata menjadi Rp{last_bal:,.0f}. Disarankan menjaga saldo minimum agar tidak menyentuh limit harian."
89
- else:
90
- msg += f"\n\n📈 **Tren Saldo**: Saldo rata-rata Rp{last_bal:,.0f} terpantau stabil, memberikan jaring pengaman finansial yang baik."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
- # GENERATIVE NBO (Parallel Execution)
93
  if client_ai:
94
  merchants = u_txn.tail(2)['raw_description'].tolist()
95
- prompt = f"Advisor Bank Nagari: Nasabah {risk_lv} risk, expense {er:.1%}. Terakhir belanja di {merchants}. Beri 1 saran sangat hangat & personal (panggil Bapak/Ibu), maks 2 kalimat."
96
  try:
97
  resp = client_ai.models.generate_content(model="gemini-1.5-flash", contents=prompt)
98
  msg += f"\n\n---\n**💡 SARAN VIRTUAL ADVISOR (AI):**\n{resp.text}"
99
- except: pass # Jika gagal, saran deterministik di atas sudah cukup profesional
100
 
101
  return msg
102
 
103
  def create_viz(self, u_bal, u_txn):
104
- # Arus Kas
105
  u_txn['m'] = u_txn['date'].dt.to_period('M').dt.to_timestamp()
106
  cf = u_txn.groupby(['m', 'transaction_type'])['amount'].sum().unstack().fillna(0)
107
  fig1 = go.Figure()
@@ -109,27 +170,27 @@ class ArchonMasterEngine:
109
  fig1.add_trace(go.Bar(x=cf.index, y=cf.get('debit', 0), name='Pengeluaran', marker_color='#0514DE'))
110
  fig1.update_layout(title="Inflow vs Outflow Bulanan", barmode='group', template='plotly_white')
111
 
112
- # Saldo
113
  fig2 = go.Figure()
114
  fig2.add_trace(go.Scatter(x=u_bal['month'], y=u_bal['avg_balance'], name='Saldo Rata-rata', line=dict(color='#F7BD87', width=4)))
115
- fig2.update_layout(title="Grafik Tren Pertumbuhan Tabungan", template='plotly_white')
116
  return fig1, fig2
117
 
118
  # --- UI EXECUTION ---
119
  engine = ArchonMasterEngine()
120
 
121
- def run_archon(cust_id):
122
  res = engine.analyze(cust_id)
123
- if not res: return "## ❌ ID Tidak Valid", "Silakan masukkan ID Customer yang terdaftar (C0001 - C0120).", None, None
124
 
125
- risk_lv, score, er, u_bal, u_txn, exp, inc = res
126
- report = engine.build_expert_advice(risk_lv, score, er, u_bal, exp, inc, u_txn)
127
  p1, p2 = engine.create_viz(u_bal, u_txn)
128
 
129
  color = "#ef4444" if risk_lv == "HIGH" else ("#f59e0b" if risk_lv == "MEDIUM" else "#10b981")
130
  status_html = f"""
131
  <div class='metric-box'>
132
- <h2 style='color: #0514DE; margin:0;'>Archon Analysis Results</h2>
133
  <div style='background:{color}; color:white; padding:5px 15px; border-radius:20px; font-weight:bold; display:inline-block; margin-top:10px;'>{risk_lv} RISK LEVEL</div>
134
  <p style='margin-top:15px; font-size:1.1em;'><b>Risk Score:</b> {score:.2f} | <b>Expense Ratio:</b> {er:.1%}</p>
135
  </div>
@@ -137,31 +198,31 @@ def run_archon(cust_id):
137
  return status_html, report, p1, p2
138
 
139
  with gr.Blocks(css=custom_css) as demo:
140
- # Header dengan teks putih dan tebal (Bold)
141
  gr.HTML("""
142
  <div class='nagari-header'>
143
  <h1>ARCHON-AI</h1>
144
- <p style='color: white !important; opacity: 0.9; margin: 5px 0 0 0;'>Platform Intelijen Resiliensi Finansial & Pengelolaan Risiko</p>
145
  </div>
146
  """)
147
 
148
  with gr.Row():
149
  with gr.Column(scale=1):
150
- id_in = gr.Textbox(label="Customer ID", placeholder="C0001")
151
  btn = gr.Button("ANALYZE CUSTOMER", variant="primary")
152
  out_status = gr.HTML()
153
  gr.Markdown("---")
154
- gr.Markdown("ℹ️ **Interpretasi**: Skor ini dihasilkan secara hibrida melalui analisis riwayat saldo, rasio belanja, dan ketepatan cicilan.")
155
 
156
  with gr.Column(scale=2):
157
  with gr.Tabs():
158
- with gr.TabItem("Analysis Summary"):
159
  out_report = gr.Markdown(elem_classes="report-card")
160
  with gr.TabItem("Cashflow Insight"):
161
  plot_cf = gr.Plot()
162
  with gr.TabItem("Saving Trend"):
163
  plot_bal = gr.Plot()
164
 
165
- btn.click(fn=run_archon, inputs=id_in, outputs=[out_status, out_report, plot_cf, plot_bal])
166
 
167
  demo.launch()
 
14
  except:
15
  client_ai = None
16
 
17
+ # --- UI STYLE: BANK NAGARI (LIGHT BLUE & GOLD) ---
18
  custom_css = """
19
  @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap');
20
  body, .gradio-container { font-family: 'Plus Jakarta Sans', sans-serif !important; background-color: #FFFFFF !important; }
21
+
22
+ .nagari-header {
23
+ background: linear-gradient(135deg, #0514DE 0%, #82C3EB 100%);
24
+ padding: 35px;
25
+ border-radius: 15px;
26
+ border-bottom: 6px solid #F7BD87;
27
+ margin-bottom: 25px;
28
+ text-align: center;
29
+ }
30
+
31
+ /* Memastikan teks ARCHON-AI BOLD PUTIH */
32
+ .nagari-header h1 {
33
+ color: #FFFFFF !important;
34
+ font-weight: 800 !important;
35
+ margin: 0;
36
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
37
+ }
38
+
39
+ .report-card {
40
+ background: #FFFFFF;
41
+ border-radius: 15px;
42
+ padding: 25px;
43
+ border: 1.5px solid #E0EDF4;
44
+ box-shadow: 0 4px 15px rgba(5, 20, 222, 0.05);
45
+ }
46
+
47
+ .status-card {
48
+ background: #E0EDF4;
49
+ padding: 20px;
50
+ border-radius: 12px;
51
+ border-left: 8px solid #0514DE;
52
+ margin-bottom: 20px;
53
+ }
54
+
55
+ .nbo-box {
56
+ background: #fffdf0;
57
+ border: 2px solid #F7BD87;
58
+ padding: 20px;
59
+ border-radius: 10px;
60
+ margin-top: 15px;
61
+ }
62
  """
63
 
64
  class ArchonMasterEngine:
 
66
  self.load_data()
67
 
68
  def load_data(self):
 
69
  try:
70
+ # Proteksi ID error (C0014, dll) dengan sanitasi data awal
71
+ self.df_txn = pd.read_csv('transactions.csv', parse_dates=['date']).fillna({"raw_description": "Transaksi Umum"})
72
  self.df_cust = pd.read_csv('customers.csv').fillna(0)
73
  self.df_bal = pd.read_csv('balances_revised.csv', parse_dates=['month']).fillna(0)
74
  self.df_rep = pd.read_csv('repayments_revised.csv', parse_dates=['due_date']).fillna("on_time")
 
76
  print(f"File Error: {e}")
77
 
78
  def analyze(self, customer_id):
79
+ # Sanitasi ID: Hapus spasi dan jadikan huruf besar
80
  cid = str(customer_id).strip().upper()
81
+
82
  u_txn = self.df_txn[self.df_txn['customer_id'] == cid].copy()
83
  u_bal = self.df_bal[self.df_bal['customer_id'] == cid].sort_values('month')
84
  u_rep = self.df_rep[self.df_rep['customer_id'] == cid]
 
94
  expense = u_txn[u_txn['transaction_type'] == 'debit']['amount'].sum()
95
  er = min(expense / ref_income, 1.0) if ref_income > 0 else 1.0
96
 
97
+ # Penentuan Discretionary Ratio (Estimasi Fase 2)
98
+ disc_keywords = ['online', 'shopee', 'tokopedia', 'cafe', 'resto', 'cinema', 'travel', 'fashion', 'promo']
99
+ disc_spending = u_txn[(u_txn['transaction_type'] == 'debit') &
100
+ (u_txn['raw_description'].str.lower().str.contains('|'.join(disc_keywords)))]['amount'].sum()
101
+ disc_ratio = disc_spending / expense if expense > 0 else 0
102
+
103
  # Scoring logic (Fase 4 - Bobot 30/20/20/20/10)
104
  er_s = 1.0 if er > 0.8 else (0.5 if er > 0.5 else 0.0)
105
  bt_s = 1.0 if len(u_bal) >= 2 and u_bal.iloc[-1]['avg_balance'] < u_bal.iloc[-2]['avg_balance'] else 0.0
 
109
  score = (0.3 * er_s) + (0.2 * bt_s) + (0.2 * od_s) + (0.2 * mp_s) + 0.1
110
  risk_lv = "HIGH" if score >= 0.7 else ("MEDIUM" if score >= 0.4 else "LOW")
111
 
112
+ return risk_lv, score, er, disc_ratio, u_bal, u_txn, expense, ref_income, mp_s
113
 
114
+ def build_expert_nbo(self, risk_lv, score, er, disc_ratio, u_bal, expense, income, mp_s, cid, u_txn):
115
+ # --- FASE 5: NBO DECISION ENGINE (LOGIKA ADAPTIF) ---
116
+ msg = f"### 📊 LAPORAN INTELIJEN ARCHON\n"
 
117
  msg += f"Berdasarkan profil mutasi, tingkat resiliensi Bapak/Ibu berada di level **{risk_lv}** (Skor: {score:.2f}).\n\n"
118
 
119
+ # 1. PENJELASAN METRIK (FASE 6)
120
+ msg += f"**Analisis Arus Kas:**\n"
121
+ msg += f"- **Rasio Pengeluaran ({er:.1%})**: Anda menghabiskan Rp{expense:,.0f} dari pendapatan Rp{income:,.0f}. "
122
+ if er > 0.8: msg += "⚠️ Kondisi ini kritis bagi stabilitas jangka panjang."
123
+
 
 
 
 
124
  if not u_bal.empty:
125
  last_bal = u_bal.iloc[-1]['avg_balance']
126
+ msg += f"\n- **Stabilitas Saldo**: Saldo rata-rata terakhir Rp{last_bal:,.0f}. "
127
+ if len(u_bal) > 1 and last_bal < u_bal.iloc[-2]['avg_balance']: msg += "📉 Tren menurun terpantau."
128
+
129
+ # 2. PENENTUAN AKSI NBO (FASE 5)
130
+ action = ""
131
+ reason = ""
132
+
133
+ if risk_lv == "HIGH" or mp_s == 1:
134
+ action = "Restructuring Suggestion"
135
+ reason = "Terdeteksi tekanan pada pembayaran cicilan atau saldo kritis. Fokus pada penyelamatan likuiditas."
136
+ elif disc_ratio > 0.6:
137
+ action = "Spending Control"
138
+ reason = "Pengeluaran gaya hidup (discretionary) melebihi 60%. Perlu pembatasan transaksi non-esensial segera."
139
+ elif risk_lv == "MEDIUM" and disc_ratio > 0.4:
140
+ action = "Budgeting Alert"
141
+ reason = "Pola belanja mulai tidak stabil. Disarankan mengaktifkan fitur notifikasi budget."
142
+ elif risk_lv == "LOW" and disc_ratio <= 0.3:
143
+ action = "Promote Investment / Saving Boost"
144
+ reason = "Kondisi finansial sangat prima dengan surplus dana. Waktunya memaksimalkan pertumbuhan aset."
145
+ else:
146
+ action = "Financial Education"
147
+ reason = "Stabilitas terjaga, namun diperlukan literasi untuk pengelolaan cashflow yang lebih optimal."
148
+
149
+ msg += f"\n\n<div class='nbo-box'>**🎯 REKOMENDASI AKSI (NBO):**\n"
150
+ msg += f"**Tindakan**: {action}\n\n"
151
+ msg += f"**Alasan**: {reason}</div>"
152
 
153
+ # 3. GENERATIVE NBO (PARALLEL EXECUTION)
154
  if client_ai:
155
  merchants = u_txn.tail(2)['raw_description'].tolist()
156
+ prompt = f"Advisor Bank Nagari: Nasabah {cid} risiko {risk_lv}, expense {er:.1%}, aksi {action}. Terakhir belanja di {merchants}. Beri saran sangat hangat & personal (panggil Bapak/Ibu), maks 2 kalimat."
157
  try:
158
  resp = client_ai.models.generate_content(model="gemini-1.5-flash", contents=prompt)
159
  msg += f"\n\n---\n**💡 SARAN VIRTUAL ADVISOR (AI):**\n{resp.text}"
160
+ except: pass
161
 
162
  return msg
163
 
164
  def create_viz(self, u_bal, u_txn):
165
+ # Grafik Inflow vs Outflow
166
  u_txn['m'] = u_txn['date'].dt.to_period('M').dt.to_timestamp()
167
  cf = u_txn.groupby(['m', 'transaction_type'])['amount'].sum().unstack().fillna(0)
168
  fig1 = go.Figure()
 
170
  fig1.add_trace(go.Bar(x=cf.index, y=cf.get('debit', 0), name='Pengeluaran', marker_color='#0514DE'))
171
  fig1.update_layout(title="Inflow vs Outflow Bulanan", barmode='group', template='plotly_white')
172
 
173
+ # Grafik Tren Saldo
174
  fig2 = go.Figure()
175
  fig2.add_trace(go.Scatter(x=u_bal['month'], y=u_bal['avg_balance'], name='Saldo Rata-rata', line=dict(color='#F7BD87', width=4)))
176
+ fig2.update_layout(title="Grafik Tren Kesehatan Saldo", template='plotly_white')
177
  return fig1, fig2
178
 
179
  # --- UI EXECUTION ---
180
  engine = ArchonMasterEngine()
181
 
182
+ def run_app(cust_id):
183
  res = engine.analyze(cust_id)
184
+ if not res: return "## ❌ ID Tidak Valid", "Masukkan ID Customer yang terdaftar (C0001 - C0120).", None, None
185
 
186
+ risk_lv, score, er, disc_ratio, u_bal, u_txn, exp, inc, mp_s = res
187
+ report = engine.build_expert_nbo(risk_lv, score, er, disc_ratio, u_bal, exp, inc, mp_s, cust_id, u_txn)
188
  p1, p2 = engine.create_viz(u_bal, u_txn)
189
 
190
  color = "#ef4444" if risk_lv == "HIGH" else ("#f59e0b" if risk_lv == "MEDIUM" else "#10b981")
191
  status_html = f"""
192
  <div class='metric-box'>
193
+ <h2 style='color: #0514DE; margin:0;'>Archon Insight Summary</h2>
194
  <div style='background:{color}; color:white; padding:5px 15px; border-radius:20px; font-weight:bold; display:inline-block; margin-top:10px;'>{risk_lv} RISK LEVEL</div>
195
  <p style='margin-top:15px; font-size:1.1em;'><b>Risk Score:</b> {score:.2f} | <b>Expense Ratio:</b> {er:.1%}</p>
196
  </div>
 
198
  return status_html, report, p1, p2
199
 
200
  with gr.Blocks(css=custom_css) as demo:
201
+ # Header Bold Putih
202
  gr.HTML("""
203
  <div class='nagari-header'>
204
  <h1>ARCHON-AI</h1>
205
+ <p style='color: white !important; opacity: 0.9; margin: 5px 0 0 0;'>Pusat Intelijen Risiko & Resiliensi Finansial Nasabah</p>
206
  </div>
207
  """)
208
 
209
  with gr.Row():
210
  with gr.Column(scale=1):
211
+ id_in = gr.Textbox(label="Input Customer ID", placeholder="C0001")
212
  btn = gr.Button("ANALYZE CUSTOMER", variant="primary")
213
  out_status = gr.HTML()
214
  gr.Markdown("---")
215
+ gr.Markdown("ℹ️ **Interpretasi**: Skor risiko menggabungkan data mutasi saldo harian, rasio belanja, dan ketepatan cicilan nasabah secara otomatis.")
216
 
217
  with gr.Column(scale=2):
218
  with gr.Tabs():
219
+ with gr.TabItem("Analysis & NBO Recommendation"):
220
  out_report = gr.Markdown(elem_classes="report-card")
221
  with gr.TabItem("Cashflow Insight"):
222
  plot_cf = gr.Plot()
223
  with gr.TabItem("Saving Trend"):
224
  plot_bal = gr.Plot()
225
 
226
+ btn.click(fn=run_app, inputs=id_in, outputs=[out_status, out_report, plot_cf, plot_bal])
227
 
228
  demo.launch()