ZakyF commited on
Commit
c6bf683
·
1 Parent(s): 9bbceff
Files changed (1) hide show
  1. app.py +56 -55
app.py CHANGED
@@ -6,18 +6,19 @@ import plotly.graph_objects as go
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,14 +28,14 @@ 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]
@@ -42,9 +43,8 @@ class ArchonNagariEngine:
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
@@ -59,83 +59,84 @@ class ArchonNagariEngine:
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 ---
95
  engine = ArchonNagariEngine()
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()
 
6
  from google import genai
7
  from transformers import pipeline
8
 
9
+ # --- KONFIGURASI AI ---
10
  GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
11
  client = genai.Client(api_key=GOOGLE_API_KEY)
12
 
13
+ # --- PALETTE WARNA & CSS CUSTOM ---
14
+ # Primary: #0514DE, Secondary: #82C3EB, Pale: #E0EDF4, Accent: #F7BD87, White: #FFFFFF
15
  custom_css = """
16
  @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700&display=swap');
17
+ .gradio-container { font-family: 'Plus Jakarta Sans', sans-serif !important; background-color: #FFFFFF !important; }
18
+ .nagari-header { background: linear-gradient(135deg, #0514DE 0%, #82C3EB 100%); color: white; padding: 30px; border-radius: 15px; border-bottom: 6px solid #F7BD87; margin-bottom: 25px; text-align: center; }
19
+ .card-style { background: #E0EDF4; border-radius: 12px; padding: 20px; border: 1px solid #82C3EB; box-shadow: 2px 4px 10px rgba(5, 20, 222, 0.05); }
20
+ .status-badge { display: inline-block; padding: 5px 15px; border-radius: 20px; font-weight: bold; color: white; }
21
+ .advice-box { background: white; border-left: 5px solid #F7BD87; padding: 20px; border-radius: 10px; font-size: 1.05em; line-height: 1.6; }
22
  """
23
 
24
  class ArchonNagariEngine:
 
28
  except: self.classifier = None
29
 
30
  def load_data(self):
31
+ # Fix C0014: fillna memastikan data siap olah
32
+ self.df_txn = pd.read_csv('transactions.csv', parse_dates=['date']).fillna({"raw_description": "Transaksi Umum"})
33
  self.df_cust = pd.read_csv('customers.csv')
34
  self.df_bal = pd.read_csv('balances_revised.csv', parse_dates=['month']).fillna(0)
35
  self.df_rep = pd.read_csv('repayments_revised.csv', parse_dates=['due_date']).fillna("on_time")
36
 
37
  def analyze(self, customer_id):
38
+ # Filter Data
39
  u_txn = self.df_txn[self.df_txn['customer_id'] == customer_id].copy()
40
  u_bal = self.df_bal[self.df_bal['customer_id'] == customer_id].sort_values('month')
41
  u_rep = self.df_rep[self.df_rep['customer_id'] == customer_id]
 
43
 
44
  if u_txn.empty or u_bal.empty: return None
45
 
46
+ # --- FASE 4: RISK SCORING (WEIGHTED) ---
47
  income_txn = u_txn[u_txn['transaction_type'] == 'credit']['amount'].sum()
 
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
 
59
 
60
  return risk_lv, score, er, u_bal, u_txn, base_income, expense
61
 
62
+ def get_adaptive_advice(self, risk_lv, er, score, u_txn, base_income, expense):
63
+ # FASE 5 & 6: ADAPTIVE GEN-AI EXPLANATION
64
+ recent_txn = u_txn.tail(3)['raw_description'].tolist()
65
+ # Prompt yang lebih teknis dan mendalam agar Gemini tidak template
66
  prompt = f"""
67
+ Identitas: Anda adalah Senior Financial Advisor Archon di Bank Nagari.
68
+ Analisis Data Nasabah:
69
+ - Status Risiko: {risk_lv} (Skor: {score:.2f}/1.00)
70
+ - Rasio Pengeluaran: {er:.2%} (Pengeluaran: Rp{expense:,.0f} dari Pendapatan: Rp{base_income:,.0f})
71
+ - Riwayat Belanja Terakhir: {', '.join(recent_txn)}
72
+
73
+ Tugas:
74
+ 1. Jelaskan arti skor {score:.2f} dan level {risk_lv} secara singkat dan logis.
75
+ 2. Berikan saran personal yang dihubungkan dengan pengeluaran {er:.2%} dan riwayat belanja tersebut.
76
+ 3. Gunakan sapaan Bapak/Ibu, bahasa Indonesia yang hangat namun profesional.
77
+ 4. Jangan gunakan pembukaan template, langsung ke inti analisis.
78
  """
79
  try:
80
  resp = client.models.generate_content(model="gemini-1.5-flash", contents=prompt)
81
  return resp.text
82
+ except:
83
+ return "Mohon maaf, sistem advisor sedang melakukan pemeliharaan. Silakan merujuk pada metrik risiko di panel kiri."
84
 
85
  def create_plots(self, u_bal, u_txn):
86
+ # Plot 1: Cashflow (Nagari Colors)
87
  u_txn['m'] = u_txn['date'].dt.to_period('M').dt.to_timestamp()
88
  cf = u_txn.groupby(['m', 'transaction_type'])['amount'].sum().unstack().fillna(0)
89
  fig1 = go.Figure()
90
+ fig1.add_trace(go.Bar(x=cf.index, y=cf.get('credit', 0), name='Pemasukan', marker_color='#82C3EB'))
91
+ fig1.add_trace(go.Bar(x=cf.index, y=cf.get('debit', 0), name='Pengeluaran', marker_color='#0514DE'))
92
+ fig1.update_layout(title="Arus Kas Bulanan", barmode='group', template='plotly_white')
93
 
94
+ # Plot 2: Balance History
95
  fig2 = go.Figure()
96
+ fig2.add_trace(go.Scatter(x=u_bal['month'], y=u_bal['avg_balance'], name='Saldo Rata-rata', line=dict(color='#F7BD87', width=4)))
97
+ fig2.update_layout(title="Tren Saldo", template='plotly_white')
 
98
  return fig1, fig2
99
 
100
  # --- UI LOGIC ---
101
  engine = ArchonNagariEngine()
102
 
103
+ def process(cust_id):
104
  res = engine.analyze(cust_id)
105
+ if not res: return "## ❌ ID Tidak Valid", "Data tidak ditemukan.", None, None
106
 
107
  risk_lv, score, er, u_bal, u_txn, inc, exp = res
108
+ advice = engine.get_adaptive_advice(risk_lv, er, score, u_txn, inc, exp)
109
  f1, f2 = engine.create_plots(u_bal, u_txn)
110
 
111
+ color = "#ef4444" if risk_lv == "HIGH" else ("#f59e0b" if risk_lv == "MEDIUM" else "#10b981")
 
 
 
 
 
 
112
 
113
+ status_html = f"""
114
+ <div class='card-style'>
115
+ <h2 style='color: #0514DE; margin:0;'>Hasil Analisis AI</h2>
116
+ <p style='margin:10px 0;'>Level Risiko: <span class='status-badge' style='background:{color}'>{risk_lv}</span></p>
117
+ <p><b>Risk Score:</b> {score:.2f} / 1.00</p>
118
+ <p><b>Expense Ratio:</b> {er:.1%}</p>
119
  </div>
120
  """
121
+ return status_html, advice, f1, f2
122
 
123
  with gr.Blocks(css=custom_css) as demo:
124
+ gr.HTML("<div class='nagari-header'><h1>🛡�� ARCHON-AI: FINANCIAL ADVISOR</h1><p>Inteligensi Manajemen Risiko & Resiliensi Perbankan</p></div>")
125
 
126
  with gr.Row():
127
  with gr.Column(scale=1):
128
+ id_in = gr.Textbox(label="Customer ID", placeholder="C0001 - C0120")
129
+ btn = gr.Button("RUN ANALYSIS", variant="primary")
130
+ out_status = gr.HTML()
 
131
  gr.Markdown("---")
132
+ gr.Markdown("**Interpretasi Metrik:**\n* **Risk Score**: Evaluasi gabungan saldo, cicilan, dan belanja.\n* **Expense Ratio**: Persentase gaji yang terpakai bulan ini.")
133
 
134
  with gr.Column(scale=2):
135
  with gr.Tabs():
136
+ with gr.TabItem("Inflow vs Outflow"): plot_1 = gr.Plot()
137
+ with gr.TabItem("Tren Pertumbuhan Saldo"): plot_2 = gr.Plot()
138
+ out_advice = gr.Markdown(elem_classes="advice-box")
139
 
140
+ btn.click(fn=process, inputs=id_in, outputs=[out_status, out_advice, plot_1, plot_2])
141
 
142
  demo.launch()