File size: 10,828 Bytes
d2169e1 eb3147c ce56fb1 d0e945e 9d82a8d 1e4d367 88eb790 552dfdc 77b155b 8cb9c5e def3f23 5af9f11 afee803 5b1f45d 9243a73 5b1f45d 9243a73 def3f23 046b27e 5af9f11 9243a73 ce56fb1 5af9f11 1e4d367 d0e945e 046b27e 06c006c 88eb790 404a444 0e5c866 9243a73 404a444 9243a73 2d79f8e 9243a73 046b27e 641143e 9243a73 046b27e 9243a73 7171bbb fc91167 9243a73 def3f23 7171bbb def3f23 d2169e1 def3f23 9243a73 5b1f45d 9243a73 def3f23 9243a73 641143e 9243a73 88eb790 9243a73 88eb790 9243a73 88eb790 7171bbb 88eb790 7171bbb 9243a73 cc1a9d9 7171bbb 9243a73 0e5c866 88eb790 9243a73 def3f23 7171bbb 9243a73 7171bbb 641143e 7171bbb 046b27e 5b1f45d 641143e 046b27e 9243a73 77b155b 5b1f45d def3f23 5b1f45d 77b155b def3f23 9243a73 def3f23 9243a73 046b27e def3f23 9243a73 9d82a8d 7171bbb 88eb790 9243a73 9d82a8d 7171bbb 9243a73 ce651b0 88eb790 def3f23 641143e 7171bbb def3f23 7171bbb 88eb790 9243a73 9d82a8d 9bbceff 4604dc0 d48cca8 d0e945e 06c006c 9243a73 dd31fa2 d0e945e d2169e1 7171bbb 88eb790 7171bbb cc1a9d9 9243a73 cc1a9d9 9243a73 046b27e 7171bbb 9243a73 cc1a9d9 9243a73 ce56fb1 9bbceff | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | import os
import pandas as pd
import numpy as np
import gradio as gr
import plotly.graph_objects as go
from google import genai
# --- CONFIG AI ---
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
client_ai = genai.Client(api_key=GOOGLE_API_KEY) if GOOGLE_API_KEY else None
# --- UI STYLE: BANK NAGARI PREMIUM ---
custom_css = """
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap');
body, .gradio-container { font-family: 'Plus Jakarta Sans', sans-serif !important; background-color: #FFFFFF !important; }
.nagari-header {
background: linear-gradient(135deg, #05A4DE 0%, #82C3EB 100%);
padding: 35px; border-radius: 15px; border-bottom: 6px solid #F7BD87;
margin-bottom: 25px; text-align: center;
}
.nagari-header h1 { color: #FFFFFF !important; font-weight: 800 !important; margin: 0; font-size: 2.2em; text-shadow: 2px 2px 4px rgba(0,0,0,0.2); }
.card-sidebar {
background: #E0EDF4; border-radius: 15px; padding: 25px;
border: 1.5px solid #82C3EB; box-shadow: 0 4px 12px rgba(5, 164, 222, 0.1);
}
.health-badge { background: white; padding: 12px; border-radius: 8px; margin-bottom: 12px; border-left: 5px solid #05A4DE; font-size: 0.95em; }
.report-card { background: white; border-radius: 12px; padding: 30px; border: 1px solid #E2E8F0; line-height: 1.8; color: #1e293b; }
.nbo-box { background: #fffdf0; border: 2px solid #F7BD87; padding: 20px; border-radius: 10px; margin-top: 20px; }
"""
class ArchonMasterEngine:
def __init__(self):
self.load_data()
def load_data(self):
try:
self.df_txn = pd.read_csv('transactions.csv', parse_dates=['date']).sort_values('date')
self.df_cust = pd.read_csv('customers.csv')
self.df_bal = pd.read_csv('balances_revised.csv', parse_dates=['month']).sort_values('month')
self.df_rep = pd.read_csv('repayments_revised.csv', parse_dates=['due_date']).fillna("on_time")
except Exception as e: print(f"Load Error: {e}")
def analyze(self, customer_id):
cid = str(customer_id).strip().upper()
u_txn = self.df_txn[self.df_txn['customer_id'] == cid].copy()
u_bal = self.df_bal[self.df_bal['customer_id'] == cid].sort_values('month')
u_rep = self.df_rep[self.df_rep['customer_id'] == cid]
u_info_df = self.df_cust[self.df_cust['customer_id'] == cid]
if u_txn.empty or u_info_df.empty: return None
u_info = u_info_df.iloc[0]
# --- FASE 2: INTELLIGENCE (Semantic Parser) ---
essential_keywords = ['indomaret', 'alfamart', 'listrik', 'pdam', 'telkom', 'sekolah', 'rs ', 'obat', 'cicilan', 'pinjaman', 'gaji', 'asuransi', 'grocer', 'utilities']
def classify_exp(row):
desc = str(row.get('raw_description', '')).lower()
return 'essential' if any(k in desc for k in essential_keywords) else 'discretionary'
u_txn['expense_type'] = u_txn.apply(classify_exp, axis=1)
# --- FASE 3 & 4: RISK SCORING ---
income_txn = u_txn[u_txn['transaction_type'] == 'credit']['amount'].sum()
ref_income = max(income_txn, u_info['monthly_income'])
expense = u_txn[u_txn['transaction_type'] == 'debit']['amount'].sum()
er = expense / ref_income if ref_income > 0 else 1.0
# Bobot 30/20/20/20/10
er_s = 1.0 if er > 0.8 else (0.5 if er > 0.5 else 0.0)
bt_s = 1.0 if len(u_bal) >= 2 and u_bal.iloc[-1]['avg_balance'] < u_bal.iloc[0]['avg_balance'] else 0.0
od_s = 1.0 if (u_bal['min_balance'] <= 0).any() else 0.0
mp_s = 1.0 if (u_rep['status'] == 'late').any() else 0.0
score = (0.3 * er_s) + (0.2 * bt_s) + (0.2 * od_s) + (0.2 * mp_s) + 0.05
risk_lv = "HIGH" if score >= 0.7 else ("MEDIUM" if score >= 0.4 else "LOW")
# --- FASE 5: NBO ENGINE ---
if risk_lv == "HIGH" or mp_s == 1:
action, nbo_msg = "Program Restrukturisasi", "Kami menyarankan penyesuaian jadwal pembayaran cicilan agar kondisi keuangan Anda tetap terjaga."
steps = ["Ajukan peninjauan tenor", "Konsolidasi tagihan harian", "Manfaatkan konsultasi dana."]
elif er > 0.6:
action, nbo_msg = "Pengaturan Limit Belanja (Spending Control)", "Kami mendeteksi pola belanja yang tinggi. Anda disarankan mengatur limit transaksi harian agar tabungan tetap aman."
steps = ["Atur limit harian QRIS", "Aktifkan notifikasi saldo", "Prioritaskan kebutuhan pokok."]
elif risk_lv == "LOW":
action, nbo_msg = "Optimalkan Tabungan (Promote Saving)", "Kondisi keuangan Anda sangat prima. Ini waktu yang tepat untuk menumbuhkan aset Anda melalui Deposito."
steps = ["Buka tabungan berjangka", "Pindahkan dana ke deposito", "Eksplorasi produk reksa dana."]
else:
action, nbo_msg = "Edukasi Pengelolaan Kas", "Mari perkuat daya tahan keuangan Anda dengan tips pengelolaan arus kas di aplikasi mobile kami."
steps = ["Baca artikel cerdas belanja", "Gunakan fitur budgeting", "Review pengeluaran bulanan."]
return risk_lv, score, er, u_bal, u_txn, expense, ref_income, action, nbo_msg, steps, er_s, bt_s, od_s, mp_s
def build_narrative(self, risk_lv, score, er, u_bal, exp, inc, action, nbo_msg, steps, cid, u_txn):
# FASE 6: EXPLAINABLE SUMMARY
msg = f"### LAPORAN ANALISIS RESILIENSI FINANSIAL ANDA\n\n"
msg += f"Halo Bapak/Ibu, sistem Archon menetapkan tingkat kesehatan finansial Anda ({cid}) pada kategori **{risk_lv}** (Skor: {score:.2f}).\n\n"
msg += f"**1. Mengapa Skor Ini Muncul?**\n"
msg += f"* **Efisiensi Belanja ({er:.1%})**: Anda menghabiskan Rp{exp:,.0f} dari pendapatan Rp{inc:,.0f}. "
msg += "Ini menunjukkan pengeluaran yang melebihi pendapatan (defisit)." if er > 1 else "Manajemen belanja Anda terpantau cukup terkendali."
if not u_bal.empty:
msg += f"\n* **Analisis Saldo**: Saldo rata-rata Anda Rp{u_bal.iloc[-1]['avg_balance']:,.0f}. "
msg += "Terdeteksi tren penurunan saldo, disarankan untuk mulai menabung kembali." if len(u_bal) > 1 and u_bal.iloc[-1]['avg_balance'] < u_bal.iloc[0]['avg_balance'] else "Saldo Anda tumbuh dengan stabil dan aman."
msg += f"\n\n<div class='nbo-box'>REKOMENDASI UNTUK ANDA: {action}\n\n"
msg += f"**Tujuan:** {nbo_msg}\n\n"
msg += f"**Langkah-Langkah Implementasi:**\n"
for i, step in enumerate(steps, 1): msg += f"{i}. {step}\n"
msg += f"</div>"
return msg
def create_viz(self, u_bal, u_txn):
# 1. Cashflow
u_txn['m'] = u_txn['date'].dt.to_period('M').dt.to_timestamp()
cf = u_txn.groupby(['m', 'transaction_type'])['amount'].sum().unstack().fillna(0)
f1 = go.Figure()
f1.add_trace(go.Bar(x=cf.index, y=cf.get('credit', 0), name='Pemasukan (Inflow)', marker_color='#82C3EB'))
f1.add_trace(go.Bar(x=cf.index, y=cf.get('debit', 0), name='Pengeluaran (Outflow)', marker_color='#05A4DE'))
f1.update_layout(title="Arus Kas Bulanan", barmode='group', template='plotly_white')
# 2. Pie Chart
exp_dist = u_txn[u_txn['transaction_type'] == 'debit'].groupby('expense_type')['amount'].sum().reset_index()
color_map = {'essential': '#05A4DE', 'discretionary': '#F7BD87'}
f2 = go.Figure(data=[go.Pie(
labels=exp_dist['expense_type'], values=exp_dist['amount'], hole=.4,
marker=dict(colors=[color_map.get(x, '#E0EDF4') for x in exp_dist['expense_type']])
)])
f2.update_layout(title="Komposisi Pengeluaran (Kebutuhan vs Gaya Hidup)")
# 3. Tren Saldo (Restored)
f3 = go.Figure()
f3.add_trace(go.Scatter(x=u_bal['month'], y=u_bal['avg_balance'], name='Saldo Rata-rata', line=dict(color='#F7BD87', width=4)))
f3.update_layout(title="Tren Pertumbuhan Saldo", template='plotly_white')
return f1, f2, f3
# --- UI LOGIC ---
engine = ArchonMasterEngine()
def run_app(cust_id):
res = engine.analyze(cust_id)
if not res: return "ID Tidak Valid", "Gunakan ID C0001 - C0120", None, None, None
risk_lv, score, er, u_bal, u_txn, exp, inc, action, nbo_msg, steps, er_s, bt_s, od_s, mp_s = res
report = engine.build_narrative(risk_lv, score, er, u_bal, exp, inc, action, nbo_msg, steps, cust_id, u_txn)
p1, p2, p3 = engine.create_viz(u_bal, u_txn)
color = "#ef4444" if risk_lv == "HIGH" else ("#f59e0b" if risk_lv == "MEDIUM" else "#10b981")
sidebar = f"""
<div class='card-sidebar'>
<h2 style='color: #05A4DE; margin:0;'>Status Keuangan</h2>
<div style='background:{color}; color:white; padding:10px 20px; border-radius:30px; font-weight:bold; display:inline-block; margin:15px 0;'>{risk_lv} RISK LEVEL</div>
<div class='health-badge'><b>Skor Risiko:</b> {score:.2f} / 1.00</div>
<div class='health-badge'><b>Efisiensi Belanja:</b> {er:.1%} {'⚠️' if er > 0.8 else '✔️'}</div>
<div class='health-badge'><b>Kesehatan Saldo:</b> {'🔻 Menurun' if bt_s == 1 else '🔺 Stabil'}</div>
</div>
"""
return sidebar, report, p1, p2, p3
with gr.Blocks(css=custom_css) as demo:
gr.HTML("<div class='nagari-header'><h1>ARCHON-AI</h1></div>")
with gr.Row():
with gr.Column(scale=1):
id_in = gr.Textbox(label="Customer ID", placeholder="C0001")
btn = gr.Button("ANALYZE CUSTOMER", variant="primary")
out_side = gr.HTML()
with gr.Column(scale=2):
with gr.Tabs():
with gr.Tab("Laporan Untuk Anda"):
out_report = gr.Markdown(elem_classes="report-card")
with gr.Tab("Visualisasi Keuangan"):
gr.Markdown("### 1. Uang Masuk vs Uang Keluar")
plot_cf = gr.Plot()
gr.HTML("<div style='background:#f1f5f9; padding:15px; border-radius:8px;'><b>Interpretasi:</b> Batang muda (Inflow) idealnya lebih tinggi dari batang tua (Outflow).</div>")
gr.Markdown("---")
gr.Markdown("### 2. Komposisi Gaya Hidup")
plot_dist = gr.Plot()
gr.HTML("<div style='background:#f1f5f9; padding:15px; border-radius:8px;'><b>Warna:</b> Biru = Kebutuhan (Essential). Emas = Gaya Hidup (Discretionary).</div>")
gr.Markdown("---")
gr.Markdown("### 3. Tren Pertumbuhan Saldo")
plot_bal = gr.Plot()
gr.HTML("<div style='background:#f1f5f9; padding:15px; border-radius:8px;'><b>Interpretasi:</b> Menunjukkan daya tahan tabungan Anda terhadap krisis.</div>")
btn.click(fn=run_app, inputs=id_in, outputs=[out_side, out_report, plot_cf, plot_dist, plot_bal])
demo.launch() |