ZakyF commited on
Commit
9bbceff
·
1 Parent(s): ce651b0
Files changed (1) hide show
  1. app.py +57 -50
app.py CHANGED
@@ -6,17 +6,18 @@ import plotly.graph_objects as go
6
  from google import genai
7
  from transformers import pipeline
8
 
9
- # --- AI SETUP ---
10
  GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
11
  client = genai.Client(api_key=GOOGLE_API_KEY)
12
 
13
- # --- STYLE BANK NAGARI (BIRU & EMAS) ---
14
  custom_css = """
15
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
16
- .gradio-container { font-family: 'Inter', sans-serif !important; background-color: #f1f5f9 !important; }
17
- .nagari-header { background: linear-gradient(135deg, #003366 0%, #004080 100%); color: white; padding: 25px; border-radius: 12px; border-bottom: 5px solid #FFD700; margin-bottom: 20px; text-align: center; }
18
- .card-nagari { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.05); border-left: 6px solid #FFD700; }
19
- .explanation-box { background: #e0f2fe; padding: 15px; border-radius: 8px; font-size: 0.9em; border: 1px solid #bae6fd; }
 
20
  """
21
 
22
  class ArchonNagariEngine:
@@ -26,24 +27,28 @@ class ArchonNagariEngine:
26
  except: self.classifier = None
27
 
28
  def load_data(self):
29
- # Fix C0014: fillna memastikan tidak ada crash saat kalkulasi statistik
30
- self.df_txn = pd.read_csv('transactions.csv', parse_dates=['date']).fillna({"raw_description": "Transaksi Umum"})
 
31
  self.df_bal = pd.read_csv('balances_revised.csv', parse_dates=['month']).fillna(0)
32
  self.df_rep = pd.read_csv('repayments_revised.csv', parse_dates=['due_date']).fillna("on_time")
33
 
34
  def analyze(self, customer_id):
 
35
  u_txn = self.df_txn[self.df_txn['customer_id'] == customer_id].copy()
36
  u_bal = self.df_bal[self.df_bal['customer_id'] == customer_id].sort_values('month')
37
  u_rep = self.df_rep[self.df_rep['customer_id'] == customer_id]
 
38
 
39
  if u_txn.empty or u_bal.empty: return None
40
 
41
- # --- FASE 4: RISK SCORING (WEIGHTED) ---
42
- income = u_txn[u_txn['transaction_type'] == 'credit']['amount'].sum()
 
 
43
  expense = u_txn[u_txn['transaction_type'] == 'debit']['amount'].sum()
44
- er = expense / income if income > 0 else 1.1
45
 
46
- # Scoring berdasarkan Bobot Manajemen
47
  er_s = 1.0 if er > 0.8 else (0.5 if er > 0.5 else 0.0)
48
  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
49
  od_s = 1.0 if (u_bal['min_balance'] <= 0).any() else 0.0
@@ -52,36 +57,38 @@ class ArchonNagariEngine:
52
  score = (0.3 * er_s) + (0.2 * bt_s) + (0.2 * od_s) + (0.2 * mp_s) + 0.1
53
  risk_lv = "HIGH" if score >= 0.7 else ("MEDIUM" if score >= 0.4 else "LOW")
54
 
55
- return risk_lv, score, er, u_bal, u_txn
56
 
57
- def get_smart_advice(self, risk_lv, er, cust_id, u_txn):
58
- # FASE 5: NBO DENGAN DATA KONTEKSTUAL
59
- merchants = u_txn[u_txn['transaction_type'] == 'debit'].tail(3)['raw_description'].tolist()
60
  prompt = f"""
61
- Halo Gemini, bertindaklah sebagai Virtual Advisor Bank Nagari.
62
- Nasabah {cust_id} berisiko {risk_lv} dengan rasio belanja {er:.1%}.
63
- Transaksi terakhir mereka di: {', '.join(merchants)}.
64
- Beri saran finansial yang SANGAT personal (bukan template!), gunakan sapaan 'Bapak/Ibu'.
65
- Hubungkan saranmu dengan merchant transaksi terakhir mereka tadi. Maks 3 kalimat.
 
66
  """
67
  try:
68
  resp = client.models.generate_content(model="gemini-1.5-flash", contents=prompt)
69
  return resp.text
70
- except: return "Bapak/Ibu, kami menyarankan efisiensi pada pos pengeluaran bulan ini agar resiliensi finansial tetap terjaga."
71
 
72
- def create_viz(self, u_bal, u_txn):
73
- # Grafik Inflow vs Outflow
74
  u_txn['m'] = u_txn['date'].dt.to_period('M').dt.to_timestamp()
75
  cf = u_txn.groupby(['m', 'transaction_type'])['amount'].sum().unstack().fillna(0)
76
  fig1 = go.Figure()
77
  fig1.add_trace(go.Bar(x=cf.index, y=cf.get('credit', 0), name='Pemasukan', marker_color='#10b981'))
78
  fig1.add_trace(go.Bar(x=cf.index, y=cf.get('debit', 0), name='Pengeluaran', marker_color='#003366'))
79
- fig1.update_layout(title="Arus Kas Bulanan", barmode='group', template='plotly_white')
80
 
81
- # Grafik Tren Saldo
82
  fig2 = go.Figure()
83
- fig2.add_trace(go.Scatter(x=u_bal['month'], y=u_bal['avg_balance'], name='Rata-rata Saldo', line=dict(color='#FFD700', width=4)))
84
- fig2.update_layout(title="Tren Saldo Nasabah", template='plotly_white')
 
85
  return fig1, fig2
86
 
87
  # --- UI LOGIC ---
@@ -89,46 +96,46 @@ engine = ArchonNagariEngine()
89
 
90
  def run_app(cust_id):
91
  res = engine.analyze(cust_id)
92
- if not res: return "### ❌ ID Tidak Valid", "Mohon gunakan ID C0001 - C0120", None, None, ""
93
 
94
- risk_lv, score, er, u_bal, u_txn = res
95
- advice = engine.get_smart_advice(risk_lv, er, cust_id, u_txn)
96
- f1, f2 = engine.create_viz(u_bal, u_txn)
97
 
98
- # Penjelasan Detail (Explainable Summary)
99
  interp = f"""
100
- ### 📖 Interpretasi Data Archon
101
- * **Risk Score ({score:.2f})**: Dihitung dari 5 parameter (Gaji, Tren Saldo, Overdraft, Cicilan, Volatilitas). Skor di atas 0.7 masuk kategori Risiko Tinggi.
102
- * **Expense Ratio ({er:.1%})**: Artinya Bapak/Ibu menghabiskan {er:.1%} dari pendapatan untuk belanja. Idealnya di bawah 50%.
103
- * **Grafik Arus Kas**: Jika batang Biru (Pengeluaran) sering lebih tinggi dari Hijau (Pemasukan), ini sinyal bahaya bagi resiliensi keuangan.
104
  """
105
 
106
  report_html = f"""
107
- <div style='padding: 20px; border-radius: 10px; background: white; border-left: 10px solid {"#ef4444" if risk_lv=="HIGH" else "#f59e0b"};'>
108
- <h2 style='color: #003366; margin: 0;'>🛡️ STATUS: {risk_lv}</h2>
109
  <p style='margin: 5px 0;'>ID Nasabah: {cust_id} | Skor: {score:.2f}</p>
110
  </div>
111
  """
112
  return report_html, advice, f1, f2, interp
113
 
114
- with gr.Blocks() as demo:
115
- gr.HTML("<div class='nagari-header'><h1>🏦 ARCHON-AI: BANK NAGARI</h1><p>Financial Resilience & Intelligent NBO Engine</p></div>")
116
 
117
  with gr.Row():
118
  with gr.Column(scale=1):
119
- with gr.Column(elem_classes="card-nagari"):
120
- id_in = gr.Textbox(label="Masukkan Customer ID", placeholder="C0001")
121
- btn = gr.Button("PROSES ANALISIS", variant="primary")
122
  out_status = gr.HTML()
123
  gr.Markdown("---")
124
- out_interp = gr.Markdown(elem_classes="explanation-box")
125
 
126
  with gr.Column(scale=2):
127
  with gr.Tabs():
128
- with gr.TabItem("Analisis Arus Kas"): plot_1 = gr.Plot()
129
- with gr.TabItem("Tren Saldo"): plot_2 = gr.Plot()
130
- out_advice = gr.Textbox(label="Virtual Advisor (Gemini 1.5 Flash)", lines=5)
131
 
132
  btn.click(fn=run_app, inputs=id_in, outputs=[out_status, out_advice, plot_1, plot_2, out_interp])
133
 
134
- demo.launch(css=custom_css)
 
6
  from google import genai
7
  from transformers import pipeline
8
 
9
+ # --- KONFIGURASI GENERATIVE AI ---
10
  GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
11
  client = genai.Client(api_key=GOOGLE_API_KEY)
12
 
13
+ # --- CUSTOM CSS: THEME BANK NAGARI BLUE & GOLD ---
14
  custom_css = """
15
+ @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700&display=swap');
16
+ .gradio-container { font-family: 'Plus Jakarta Sans', sans-serif !important; background-color: #f8fafc !important; }
17
+ .nagari-header { background: linear-gradient(135deg, #003366 0%, #004d99 100%); color: white; padding: 30px; border-radius: 15px; border-bottom: 6px solid #FFD700; margin-bottom: 25px; text-align: center; box-shadow: 0 4px 20px rgba(0,51,102,0.2); }
18
+ .metric-card { background: white; border-radius: 12px; padding: 20px; border-left: 6px solid #FFD700; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
19
+ .advice-card { background: #fffdf0; border: 1px solid #fde68a; padding: 20px; border-radius: 12px; font-style: italic; color: #1e293b; }
20
+ .interpretation-panel { background: #f1f5f9; padding: 20px; border-radius: 10px; border: 1px solid #e2e8f0; font-size: 0.95em; line-height: 1.6; }
21
  """
22
 
23
  class ArchonNagariEngine:
 
27
  except: self.classifier = None
28
 
29
  def load_data(self):
30
+ # Fase 1: Foundation (Fix C0014 & data mapping)
31
+ self.df_txn = pd.read_csv('transactions.csv', parse_dates=['date']).fillna({"raw_description": "Transaksi Umum", "counterparty": "Merchant"})
32
+ self.df_cust = pd.read_csv('customers.csv')
33
  self.df_bal = pd.read_csv('balances_revised.csv', parse_dates=['month']).fillna(0)
34
  self.df_rep = pd.read_csv('repayments_revised.csv', parse_dates=['due_date']).fillna("on_time")
35
 
36
  def analyze(self, customer_id):
37
+ # 1. Validasi & Filter
38
  u_txn = self.df_txn[self.df_txn['customer_id'] == customer_id].copy()
39
  u_bal = self.df_bal[self.df_bal['customer_id'] == customer_id].sort_values('month')
40
  u_rep = self.df_rep[self.df_rep['customer_id'] == customer_id]
41
+ u_info = self.df_cust[self.df_cust['customer_id'] == customer_id].iloc[0]
42
 
43
  if u_txn.empty or u_bal.empty: return None
44
 
45
+ # --- FASE 4: RISK SCORING (WEIGHTED 30/20/20/20/10) ---
46
+ income_txn = u_txn[u_txn['transaction_type'] == 'credit']['amount'].sum()
47
+ # Perbaikan Logic: Gunakan income profil jika income mutasi tidak normal
48
+ base_income = max(income_txn, u_info['monthly_income'])
49
  expense = u_txn[u_txn['transaction_type'] == 'debit']['amount'].sum()
50
+ er = expense / base_income
51
 
 
52
  er_s = 1.0 if er > 0.8 else (0.5 if er > 0.5 else 0.0)
53
  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
54
  od_s = 1.0 if (u_bal['min_balance'] <= 0).any() else 0.0
 
57
  score = (0.3 * er_s) + (0.2 * bt_s) + (0.2 * od_s) + (0.2 * mp_s) + 0.1
58
  risk_lv = "HIGH" if score >= 0.7 else ("MEDIUM" if score >= 0.4 else "LOW")
59
 
60
+ return risk_lv, score, er, u_bal, u_txn, base_income, expense
61
 
62
+ def get_generative_advice(self, risk_lv, er, cust_id, u_txn):
63
+ # FASE 5: NBO (Smart Prompting)
64
+ fav_merchant = u_txn[u_txn['transaction_type'] == 'debit']['raw_description'].mode().iloc[0] if not u_txn.empty else "transaksi harian"
65
  prompt = f"""
66
+ Identitas: Anda adalah Senior Personal Banker di Bank Nagari.
67
+ Analisis Nasabah {cust_id}: Risiko {risk_lv}, Rasio Belanja {er:.1%}.
68
+ Konteks: Nasabah sering bertransaksi di '{fav_merchant}'.
69
+ Tugas: Berikan saran finansial yang SANGAT personal, hangat (sapa Bapak/Ibu), dan tidak kaku.
70
+ Hubungkan saran Anda dengan kebiasaan belanja mereka di '{fav_merchant}' agar terasa nyata.
71
+ Maksimal 3 kalimat. Hindari kata-kata bot seperti 'berdasarkan data'.
72
  """
73
  try:
74
  resp = client.models.generate_content(model="gemini-1.5-flash", contents=prompt)
75
  return resp.text
76
+ except: return "Bapak/Ibu, mari kita tinjau kembali pola pengeluaran bulan ini agar rencana masa depan Anda tetap terjaga dengan aman."
77
 
78
+ def create_plots(self, u_bal, u_txn):
79
+ # Grafik 1: Inflow vs Outflow (Fase 6)
80
  u_txn['m'] = u_txn['date'].dt.to_period('M').dt.to_timestamp()
81
  cf = u_txn.groupby(['m', 'transaction_type'])['amount'].sum().unstack().fillna(0)
82
  fig1 = go.Figure()
83
  fig1.add_trace(go.Bar(x=cf.index, y=cf.get('credit', 0), name='Pemasukan', marker_color='#10b981'))
84
  fig1.add_trace(go.Bar(x=cf.index, y=cf.get('debit', 0), name='Pengeluaran', marker_color='#003366'))
85
+ fig1.update_layout(title="Laporan Arus Kas Bulanan", barmode='group', template='plotly_white')
86
 
87
+ # Grafik 2: Balance History
88
  fig2 = go.Figure()
89
+ fig2.add_trace(go.Scatter(x=u_bal['month'], y=u_bal['avg_balance'], name='Saldo Rata-rata', line=dict(color='#FFD700', width=4)))
90
+ fig2.add_trace(go.Bar(x=u_bal['month'], y=u_bal['min_balance'], name='Saldo Minimum', marker_color='#94a3b8', opacity=0.3))
91
+ fig2.update_layout(title="Tren Pertumbuhan Saldo", template='plotly_white')
92
  return fig1, fig2
93
 
94
  # --- UI LOGIC ---
 
96
 
97
  def run_app(cust_id):
98
  res = engine.analyze(cust_id)
99
+ if not res: return "## ❌ ID Tidak Terdaftar", "Gunakan ID C0001-C0120", None, None, ""
100
 
101
+ risk_lv, score, er, u_bal, u_txn, inc, exp = res
102
+ advice = engine.get_generative_advice(risk_lv, er, cust_id, u_txn)
103
+ f1, f2 = engine.create_plots(u_bal, u_txn)
104
 
105
+ # INTERPRETASI DATA YANG ELEGAN (Fase 6)
106
  interp = f"""
107
+ ### 🛡️ Ringkasan Intelijen Keuangan
108
+ * **Indeks Risiko ({score:.2f})**: Merupakan hasil evaluasi komprehensif terhadap 5 variabel vital. Status **{risk_lv}** mengindikasikan perlunya { 'tindakan preventif segera' if risk_lv=='HIGH' else 'pemantauan berkala' }.
109
+ * **Efisiensi Anggaran ({er:.1%})**: Bapak/Ibu mengalokasikan {er:.1%} dari total pemasukan untuk pengeluaran. Kami merekomendasikan batas ideal pengeluaran di angka 50% untuk resiliensi jangka panjang.
110
+ * **Analisis Visual**: Grafik Arus Kas menunjukkan perbandingan likuiditas. Jika batang Biru dominan, disarankan untuk melakukan restrukturisasi anggaran pada pos pengeluaran gaya hidup.
111
  """
112
 
113
  report_html = f"""
114
+ <div class='metric-card' style='border-left-color: {"#ef4444" if risk_lv=="HIGH" else "#f59e0b"};'>
115
+ <h2 style='color: #003366; margin:0;'>Status Resilience: {risk_lv}</h2>
116
  <p style='margin: 5px 0;'>ID Nasabah: {cust_id} | Skor: {score:.2f}</p>
117
  </div>
118
  """
119
  return report_html, advice, f1, f2, interp
120
 
121
+ with gr.Blocks(css=custom_css) as demo:
122
+ gr.HTML("<div class='nagari-header'><h1>🏦 ARCHON-AI: BANK NAGARI</h1><p>Pusat Intelijen Risiko & Resiliensi Finansial Nasabah</p></div>")
123
 
124
  with gr.Row():
125
  with gr.Column(scale=1):
126
+ with gr.Column(elem_classes="metric-card"):
127
+ id_in = gr.Textbox(label="Customer ID", placeholder="Masukkan ID (C0001)")
128
+ btn = gr.Button("MULAI ANALISIS", variant="primary")
129
  out_status = gr.HTML()
130
  gr.Markdown("---")
131
+ out_interp = gr.Markdown(elem_classes="interpretation-panel")
132
 
133
  with gr.Column(scale=2):
134
  with gr.Tabs():
135
+ with gr.TabItem("Arus Kas Bulanan"): plot_1 = gr.Plot()
136
+ with gr.TabItem("Riwayat Saldo"): plot_2 = gr.Plot()
137
+ out_advice = gr.Markdown(elem_classes="advice-card")
138
 
139
  btn.click(fn=run_app, inputs=id_in, outputs=[out_status, out_advice, plot_1, plot_2, out_interp])
140
 
141
+ demo.launch()