update
Browse files
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
|
| 10 |
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
|
| 11 |
client = genai.Client(api_key=GOOGLE_API_KEY)
|
| 12 |
|
| 13 |
-
# ---
|
| 14 |
custom_css = """
|
| 15 |
-
@import url('https://fonts.googleapis.com/css2?family=
|
| 16 |
-
.gradio-container { font-family: '
|
| 17 |
-
.nagari-header { background: linear-gradient(135deg, #003366 0%, #
|
| 18 |
-
.card
|
| 19 |
-
.
|
|
|
|
| 20 |
"""
|
| 21 |
|
| 22 |
class ArchonNagariEngine:
|
|
@@ -26,24 +27,28 @@ class ArchonNagariEngine:
|
|
| 26 |
except: self.classifier = None
|
| 27 |
|
| 28 |
def load_data(self):
|
| 29 |
-
#
|
| 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 |
-
|
|
|
|
|
|
|
| 43 |
expense = u_txn[u_txn['transaction_type'] == 'debit']['amount'].sum()
|
| 44 |
-
er = expense /
|
| 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
|
| 58 |
-
# FASE 5: NBO
|
| 59 |
-
|
| 60 |
prompt = f"""
|
| 61 |
-
|
| 62 |
-
Nasabah {cust_id}
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
Hubungkan
|
|
|
|
| 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,
|
| 71 |
|
| 72 |
-
def
|
| 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
|
| 82 |
fig2 = go.Figure()
|
| 83 |
-
fig2.add_trace(go.Scatter(x=u_bal['month'], y=u_bal['avg_balance'], name='Rata-rata
|
| 84 |
-
fig2.
|
|
|
|
| 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 "
|
| 93 |
|
| 94 |
-
risk_lv, score, er, u_bal, u_txn = res
|
| 95 |
-
advice = engine.
|
| 96 |
-
f1, f2 = engine.
|
| 97 |
|
| 98 |
-
#
|
| 99 |
interp = f"""
|
| 100 |
-
###
|
| 101 |
-
* **
|
| 102 |
-
* **
|
| 103 |
-
* **Grafik Arus Kas
|
| 104 |
"""
|
| 105 |
|
| 106 |
report_html = f"""
|
| 107 |
-
<div
|
| 108 |
-
<h2 style='color: #003366; margin:
|
| 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>
|
| 116 |
|
| 117 |
with gr.Row():
|
| 118 |
with gr.Column(scale=1):
|
| 119 |
-
with gr.Column(elem_classes="card
|
| 120 |
-
id_in = gr.Textbox(label="
|
| 121 |
-
btn = gr.Button("
|
| 122 |
out_status = gr.HTML()
|
| 123 |
gr.Markdown("---")
|
| 124 |
-
out_interp = gr.Markdown(elem_classes="
|
| 125 |
|
| 126 |
with gr.Column(scale=2):
|
| 127 |
with gr.Tabs():
|
| 128 |
-
with gr.TabItem("
|
| 129 |
-
with gr.TabItem("
|
| 130 |
-
out_advice = gr.
|
| 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(
|
|
|
|
| 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()
|