ZakyF commited on
Commit
ce651b0
·
1 Parent(s): 9d82a8d
Files changed (1) hide show
  1. app.py +63 -61
app.py CHANGED
@@ -6,47 +6,44 @@ import plotly.graph_objects as go
6
  from google import genai
7
  from transformers import pipeline
8
 
9
- # --- KONFIGURASI AI (SDK 2026) ---
10
  GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
11
  client = genai.Client(api_key=GOOGLE_API_KEY)
12
 
13
- # --- STYLE CSS BANK NAGARI ---
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: #f8fafc !important; }
17
- .nagari-header { background: linear-gradient(135deg, #800000 0%, #a52a2a 100%); color: white; padding: 25px; border-radius: 15px; border-bottom: 5px solid #FFD700; margin-bottom: 20px; }
18
- .risk-card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); border-top: 6px solid #800000; }
19
- .advice-box { background: #fffdf0; border-left: 5px solid #FFD700; padding: 15px; border-radius: 8px; }
20
  """
21
 
22
  class ArchonNagariEngine:
23
  def __init__(self):
24
  self.load_data()
25
- try:
26
- self.classifier = pipeline("text-classification", model="archon_v1", tokenizer="archon_v1")
27
- except:
28
- self.classifier = None
29
 
30
  def load_data(self):
31
- # Fix Error C0014 dengan penanganan data kosong yang lebih agresif
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').fillna(0)
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 run_analysis(self, customer_id):
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
 
42
- if u_txn.empty or u_bal.empty:
43
- return None
44
 
45
- # --- FASE 4: RISK SCORING ---
46
  income = u_txn[u_txn['transaction_type'] == 'credit']['amount'].sum()
47
  expense = u_txn[u_txn['transaction_type'] == 'debit']['amount'].sum()
48
- er = expense / income if income > 0 else 1.2
49
 
 
50
  er_s = 1.0 if er > 0.8 else (0.5 if er > 0.5 else 0.0)
51
  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
52
  od_s = 1.0 if (u_bal['min_balance'] <= 0).any() else 0.0
@@ -57,76 +54,81 @@ class ArchonNagariEngine:
57
 
58
  return risk_lv, score, er, u_bal, u_txn
59
 
60
- def get_gemini_advice(self, risk_lv, er, cust_id, u_txn):
61
- # FASE 5: NBO (NEW SDK 2026)
62
- last_txn = u_txn.tail(3)['raw_description'].tolist()
63
- prompt = f"Berikan saran finansial hangat khas perbankan (sapa Bapak/Ibu) untuk nasabah {cust_id} dengan risiko {risk_lv} dan rasio pengeluaran {er:.1%}. Transaksi terakhir: {last_txn}. Maks 3 kalimat."
64
-
 
 
 
 
 
65
  try:
66
- response = client.models.generate_content(model="gemini-1.5-flash", contents=prompt)
67
- return response.text
68
- except:
69
- return "Kami menyarankan peninjauan berkala pada pengeluaran bulanan Bapak/Ibu untuk menjaga stabilitas saldo."
70
 
71
- def create_plots(self, u_bal, u_txn):
72
- # Plot 1: Cashflow
73
  u_txn['m'] = u_txn['date'].dt.to_period('M').dt.to_timestamp()
74
  cf = u_txn.groupby(['m', 'transaction_type'])['amount'].sum().unstack().fillna(0)
75
  fig1 = go.Figure()
76
  fig1.add_trace(go.Bar(x=cf.index, y=cf.get('credit', 0), name='Pemasukan', marker_color='#10b981'))
77
- fig1.add_trace(go.Bar(x=cf.index, y=cf.get('debit', 0), name='Pengeluaran', marker_color='#800000'))
78
- fig1.update_layout(title="Inflow vs Outflow", barmode='group', template='plotly_white')
79
 
80
- # Plot 2: Balance
81
  fig2 = go.Figure()
82
- fig2.add_trace(go.Scatter(x=u_bal['month'], y=u_bal['avg_balance'], name='Saldo Rata-rata', line=dict(color='#800000', width=4)))
83
- fig2.update_layout(title="Tren Saldo", template='plotly_white')
84
-
85
  return fig1, fig2
86
 
87
- # --- UI INTERFACE ---
88
  engine = ArchonNagariEngine()
89
 
90
- def process(cust_id):
91
- res = engine.run_analysis(cust_id)
92
- if not res: return "### ⚠️ Nasabah Tidak Ditemukan", "Data tidak tersedia.", None, None
93
 
94
  risk_lv, score, er, u_bal, u_txn = res
95
- advice = engine.get_gemini_advice(risk_lv, er, cust_id, u_txn)
96
- f1, f2 = engine.create_plots(u_bal, u_txn)
 
 
 
 
 
 
 
 
97
 
98
  report_html = f"""
99
- <div style='padding: 20px; border-radius: 10px; background: white; border-left: 10px solid {"#d32f2f" if risk_lv=="HIGH" else "#f9a825"};'>
100
- <h2 style='color: #800000; margin: 0;'>Status: {risk_lv}</h2>
101
- <p><b>Risk Score:</b> {score:.2f} | <b>Expense Ratio:</b> {er:.1%}</p>
102
  </div>
103
  """
104
- return report_html, advice, f1, f2
105
 
106
  with gr.Blocks() as demo:
107
- gr.HTML("""
108
- <div class="nagari-header">
109
- <h1 style="margin:0;">🛡️ ARCHON-AI: BANK NAGARI EDITION</h1>
110
- <p style="margin:0; opacity: 0.8;">Automated Financial Resilience & Risk Intelligence</p>
111
- </div>
112
- """)
113
 
114
  with gr.Row():
115
  with gr.Column(scale=1):
116
- id_input = gr.Textbox(label="Customer ID", placeholder="C0001 - C0120")
117
- btn = gr.Button("ANALYZE NOW", variant="primary")
118
- out_report = gr.HTML()
 
 
 
119
 
120
  with gr.Column(scale=2):
121
  with gr.Tabs():
122
- with gr.TabItem("Arus Kas"):
123
- plot_cf = gr.Plot()
124
- with gr.TabItem("Tren Saldo"):
125
- plot_bal = gr.Plot()
126
-
127
- out_advice = gr.Textbox(label="Personalized NBO (Powered by Gemini 1.5)", lines=4, elem_classes="advice-box")
128
 
129
- btn.click(fn=process, inputs=id_input, outputs=[out_report, out_advice, plot_cf, plot_bal])
130
 
131
- # Jalankan dengan CSS di launch
132
  demo.launch(css=custom_css)
 
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:
23
  def __init__(self):
24
  self.load_data()
25
+ try: self.classifier = pipeline("text-classification", model="archon_v1")
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
 
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 ---
88
  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)