LamRhm commited on
Commit
29d5ceb
Β·
verified Β·
1 Parent(s): 3309173

Upload 6 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ amit_kedia_dataset.csv filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: SEC Fraud Detector
3
+ emoji: πŸ”
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: streamlit
7
+ sdk_version: 1.32.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # SEC Fraud Detector
14
+
15
+ NLP-based fraud detection on SEC financial filings.
16
+ Built as a data science bootcamp capstone project.
17
+
18
+ ## What it does
19
+
20
+ Paste any financial statement excerpt (10-K, 8-K, MD&A section) and get an instant fraud risk assessment. The model classifies the text as **Low**, **Moderate**, or **High** risk based on linguistic patterns learned from confirmed fraud cases.
21
+
22
+ ## Model
23
+
24
+ | Component | Details |
25
+ |---|---|
26
+ | **Architecture** | Logistic Regression |
27
+ | **Embedding** | Hybrid TF-IDF (500 features) + SEC-BERT (768 dims) |
28
+ | **SEC-BERT model** | `nlpaueb/sec-bert-base` β€” frozen, no fine-tuning |
29
+ | **Dataset** | AmitKedia SEC Filings β€” 170 samples (85 fraud / 85 non-fraud) |
30
+ | **Validation** | Stratified 5-fold cross-validation |
31
+ | **Test F1** | 0.895 |
32
+ | **Recall (fraud)** | 1.000 β€” zero missed fraud cases on test set |
33
+ | **ROC-AUC** | 0.983 |
34
+ | **CV F1** | 0.856 Β± 0.056 |
35
+
36
+ ## Project pipeline
37
+
38
+ ```
39
+ NB01 β€” EDA
40
+ └─ Class distribution, text length, vocabulary analysis, word clouds
41
+
42
+ NB02 β€” Embedding Comparison
43
+ └─ TF-IDF vs Frozen FinBERT vs Frozen SEC-BERT
44
+ vs Hybrid TF-IDF + SEC-BERT vs Isolation Forest (unsupervised)
45
+ └─ Winner: Hybrid TF-IDF + SEC-BERT (CV F1 = 0.845, Recall = 0.929)
46
+
47
+ NB03 β€” Classifier Comparison on Hybrid features
48
+ └─ Logistic Regression vs Linear SVM vs Random Forest
49
+ vs Gradient Boosting vs Naive Bayes
50
+ └─ Winner: Logistic Regression (Recall = 1.0, gap = 0.0004)
51
+ ```
52
+
53
+ ## Why Hybrid TF-IDF + SEC-BERT
54
+
55
+ - **TF-IDF** captures fraud-specific lexical patterns: *restatement, going concern, material weakness, irregularities*
56
+ - **SEC-BERT** (`nlpaueb/sec-bert-base`) was pre-trained on actual SEC 10-K and 10-Q filings β€” the exact document type being classified
57
+ - Combining both gives the model lexical and semantic signals simultaneously
58
+ - At 170 samples, neither embedding alone generalises as well as their combination
59
+
60
+ ## Dataset
61
+
62
+ [AmitKedia Financial Statement Fraud Dataset](https://www.kaggle.com/datasets/amitkedia/financial-statement-fraud-data) β€” 170 SEC filings, balanced 85 fraud / 85 non-fraud, sourced from SEC-EDGAR and AAER enforcement releases.
63
+
64
+ ## Scope & limitations
65
+
66
+ > ⚠️ This tool classifies filing **text** based on linguistic patterns learned from 170 SEC filings. It does not perform forensic accounting analysis or evaluate structured financial ratios. Results are probabilistic and should not replace professional audit judgment. Built for research and educational purposes only.
67
+
68
+ ## Tech stack
69
+
70
+ `Python` Β· `Streamlit` Β· `scikit-learn` Β· `HuggingFace Transformers` Β· `PyTorch` Β· `SEC-BERT`
71
+
72
+ ## Author
73
+
74
+ **Lamia Rahmani** β€” Data Science Bootcamp Capstone, 2026
75
+ **Bootcamp Coach:** Sara Latreche
amit_kedia_dataset.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9c261b2ca9c208f90b6decda3ca32eae4706421586b6d455edd7bbc60c48a7cd
3
+ size 217918716
app.py ADDED
@@ -0,0 +1,644 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pickle
3
+ import joblib
4
+ import torch
5
+ import numpy as np
6
+ import re
7
+ import os
8
+ from transformers import AutoTokenizer, AutoModel
9
+ from datetime import datetime
10
+
11
+ # ─────────────────────────────────────────────
12
+ # Page config
13
+ # ─────────────────────────────────────────────
14
+ st.set_page_config(
15
+ page_title="SEC Fraud Detector",
16
+ page_icon="πŸ”",
17
+ layout="wide",
18
+ initial_sidebar_state="collapsed"
19
+ )
20
+
21
+ # ─────────────────────────────────────────────
22
+ # CSS
23
+ # ─────────────────────────────────────────────
24
+ st.markdown("""
25
+ <style>
26
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
27
+
28
+ /* Reset */
29
+ [data-testid="stAppViewContainer"] { background: #0a0e17; }
30
+ [data-testid="stHeader"] { background: transparent; }
31
+ [data-testid="stSidebar"] { background: #0d1220; border-right: 1px solid #1e2a3a; }
32
+ html, body, [class*="css"] { font-family: 'Inter', sans-serif; }
33
+
34
+ /* Hide default streamlit chrome */
35
+ #MainMenu, footer, header { visibility: hidden; }
36
+ [data-testid="stDecoration"] { display: none; }
37
+
38
+ /* Page shell */
39
+ .main .block-container {
40
+ padding: 0 !important;
41
+ max-width: 100% !important;
42
+ }
43
+
44
+ /* ── NAV BAR ── */
45
+ .nav-bar {
46
+ display: flex;
47
+ align-items: center;
48
+ justify-content: space-between;
49
+ padding: 18px 48px;
50
+ border-bottom: 1px solid #1e2a3a;
51
+ background: #0a0e17;
52
+ position: sticky;
53
+ top: 0;
54
+ z-index: 100;
55
+ }
56
+ .nav-logo {
57
+ display: flex;
58
+ align-items: center;
59
+ gap: 10px;
60
+ font-size: 15px;
61
+ font-weight: 600;
62
+ color: #f1f5f9;
63
+ letter-spacing: -0.3px;
64
+ }
65
+ .nav-logo-dot {
66
+ width: 8px; height: 8px;
67
+ border-radius: 50%;
68
+ background: #3b82f6;
69
+ box-shadow: 0 0 8px #3b82f680;
70
+ }
71
+ .nav-badge {
72
+ font-size: 11px;
73
+ font-weight: 500;
74
+ color: #64748b;
75
+ background: #1e2a3a;
76
+ padding: 4px 10px;
77
+ border-radius: 20px;
78
+ letter-spacing: 0.5px;
79
+ }
80
+
81
+ /* ── HERO ── */
82
+ .hero {
83
+ padding: 64px 48px 48px;
84
+ max-width: 1100px;
85
+ margin: 0 auto;
86
+ }
87
+ .hero-eyebrow {
88
+ font-size: 11px;
89
+ font-weight: 600;
90
+ letter-spacing: 2px;
91
+ text-transform: uppercase;
92
+ color: #3b82f6;
93
+ margin-bottom: 16px;
94
+ }
95
+ .hero-title {
96
+ font-size: 42px;
97
+ font-weight: 700;
98
+ color: #f1f5f9;
99
+ line-height: 1.15;
100
+ letter-spacing: -1.2px;
101
+ margin-bottom: 16px;
102
+ }
103
+ .hero-title span {
104
+ color: #3b82f6;
105
+ }
106
+ .hero-desc {
107
+ font-size: 15px;
108
+ color: #64748b;
109
+ line-height: 1.7;
110
+ max-width: 560px;
111
+ margin-bottom: 40px;
112
+ }
113
+
114
+ /* ── STAT STRIP ── */
115
+ .stat-strip {
116
+ display: grid;
117
+ grid-template-columns: repeat(4, 1fr);
118
+ gap: 1px;
119
+ background: #1e2a3a;
120
+ border: 1px solid #1e2a3a;
121
+ border-radius: 12px;
122
+ overflow: hidden;
123
+ margin-bottom: 48px;
124
+ }
125
+ .stat-cell {
126
+ background: #0d1220;
127
+ padding: 20px 24px;
128
+ }
129
+ .stat-val {
130
+ font-family: 'JetBrains Mono', monospace;
131
+ font-size: 24px;
132
+ font-weight: 500;
133
+ color: #f1f5f9;
134
+ letter-spacing: -0.5px;
135
+ }
136
+ .stat-lbl {
137
+ font-size: 11px;
138
+ color: #475569;
139
+ text-transform: uppercase;
140
+ letter-spacing: 1px;
141
+ margin-top: 4px;
142
+ }
143
+
144
+ /* ── MAIN GRID ── */
145
+ .main-grid {
146
+ display: grid;
147
+ grid-template-columns: 1.4fr 1fr;
148
+ gap: 20px;
149
+ padding: 0 48px 48px;
150
+ max-width: 1100px;
151
+ margin: 0 auto;
152
+ }
153
+
154
+ /* ── CARD ── */
155
+ .card {
156
+ background: #0d1220;
157
+ border: 1px solid #1e2a3a;
158
+ border-radius: 14px;
159
+ padding: 28px;
160
+ }
161
+ .card-title {
162
+ font-size: 12px;
163
+ font-weight: 600;
164
+ letter-spacing: 1.5px;
165
+ text-transform: uppercase;
166
+ color: #475569;
167
+ margin-bottom: 20px;
168
+ }
169
+
170
+ /* ── TEXTAREA OVERRIDE ── */
171
+ textarea {
172
+ background: #070b12 !important;
173
+ border: 1px solid #1e2a3a !important;
174
+ border-radius: 10px !important;
175
+ color: #cbd5e1 !important;
176
+ font-family: 'JetBrains Mono', monospace !important;
177
+ font-size: 13px !important;
178
+ line-height: 1.7 !important;
179
+ resize: none !important;
180
+ }
181
+ textarea:focus {
182
+ border-color: #3b82f6 !important;
183
+ box-shadow: 0 0 0 2px #3b82f620 !important;
184
+ }
185
+ textarea::placeholder { color: #334155 !important; }
186
+
187
+ /* ── BUTTON ── */
188
+ [data-testid="stButton"] > button {
189
+ background: #3b82f6 !important;
190
+ color: white !important;
191
+ border: none !important;
192
+ border-radius: 8px !important;
193
+ padding: 12px 28px !important;
194
+ font-size: 13px !important;
195
+ font-weight: 600 !important;
196
+ letter-spacing: 0.3px !important;
197
+ width: 100% !important;
198
+ margin-top: 12px !important;
199
+ cursor: pointer !important;
200
+ transition: background 0.15s !important;
201
+ }
202
+ [data-testid="stButton"] > button:hover {
203
+ background: #2563eb !important;
204
+ }
205
+
206
+ /* ── RESULT PANEL ── */
207
+ .result-risk-badge {
208
+ display: inline-block;
209
+ padding: 6px 16px;
210
+ border-radius: 20px;
211
+ font-size: 12px;
212
+ font-weight: 700;
213
+ letter-spacing: 1px;
214
+ text-transform: uppercase;
215
+ margin-bottom: 24px;
216
+ }
217
+ .badge-low { background: #052e16; color: #4ade80; border: 1px solid #166534; }
218
+ .badge-mod { background: #431407; color: #fb923c; border: 1px solid #9a3412; }
219
+ .badge-high { background: #3b0764; color: #e879f9; border: 1px solid #7e22ce; }
220
+
221
+ .prob-number {
222
+ font-family: 'JetBrains Mono', monospace;
223
+ font-size: 56px;
224
+ font-weight: 500;
225
+ line-height: 1;
226
+ letter-spacing: -2px;
227
+ margin-bottom: 8px;
228
+ }
229
+ .prob-low { color: #4ade80; }
230
+ .prob-mod { color: #fb923c; }
231
+ .prob-high { color: #e879f9; }
232
+
233
+ .prob-bar-track {
234
+ height: 4px;
235
+ background: #1e2a3a;
236
+ border-radius: 4px;
237
+ margin: 20px 0 28px;
238
+ overflow: hidden;
239
+ }
240
+ .prob-bar-fill {
241
+ height: 100%;
242
+ border-radius: 4px;
243
+ transition: width 0.6s ease;
244
+ }
245
+
246
+ .metric-row {
247
+ display: flex;
248
+ justify-content: space-between;
249
+ align-items: center;
250
+ padding: 10px 0;
251
+ border-bottom: 1px solid #1e2a3a;
252
+ }
253
+ .metric-row:last-child { border-bottom: none; }
254
+ .metric-key {
255
+ font-size: 12px;
256
+ color: #475569;
257
+ font-family: 'JetBrains Mono', monospace;
258
+ }
259
+ .metric-val {
260
+ font-size: 12px;
261
+ color: #94a3b8;
262
+ font-weight: 500;
263
+ }
264
+ .metric-val-accent {
265
+ font-size: 12px;
266
+ color: #3b82f6;
267
+ font-weight: 600;
268
+ font-family: 'JetBrains Mono', monospace;
269
+ }
270
+
271
+ /* ── KEYWORD PANEL ── */
272
+ .kw-list {
273
+ display: flex;
274
+ flex-wrap: wrap;
275
+ gap: 8px;
276
+ margin-top: 8px;
277
+ }
278
+ .kw-chip {
279
+ font-family: 'JetBrains Mono', monospace;
280
+ font-size: 11px;
281
+ padding: 4px 10px;
282
+ border-radius: 6px;
283
+ background: #0a1628;
284
+ border: 1px solid #1e3a5f;
285
+ color: #60a5fa;
286
+ }
287
+ .kw-chip.hit {
288
+ background: #1e1245;
289
+ border-color: #7c3aed;
290
+ color: #c084fc;
291
+ }
292
+
293
+ /* ── MODEL INFO ── */
294
+ .info-row {
295
+ display: flex;
296
+ justify-content: space-between;
297
+ align-items: flex-start;
298
+ padding: 10px 0;
299
+ border-bottom: 1px solid #1e2a3a;
300
+ gap: 16px;
301
+ }
302
+ .info-row:last-child { border-bottom: none; }
303
+ .info-key {
304
+ font-size: 11px;
305
+ color: #475569;
306
+ text-transform: uppercase;
307
+ letter-spacing: 0.8px;
308
+ white-space: nowrap;
309
+ padding-top: 1px;
310
+ }
311
+ .info-val {
312
+ font-size: 12px;
313
+ color: #94a3b8;
314
+ text-align: right;
315
+ line-height: 1.5;
316
+ }
317
+
318
+ /* ── NOTICE ── */
319
+ .notice {
320
+ margin: 0 48px 32px;
321
+ max-width: 1060px;
322
+ background: #0d1628;
323
+ border: 1px solid #1e3a5f;
324
+ border-radius: 10px;
325
+ padding: 14px 20px;
326
+ font-size: 12px;
327
+ color: #475569;
328
+ line-height: 1.6;
329
+ }
330
+ .notice span { color: #3b82f6; font-weight: 500; }
331
+
332
+ /* Alert overrides */
333
+ [data-testid="stAlert"] {
334
+ background: transparent !important;
335
+ border: none !important;
336
+ padding: 0 !important;
337
+ }
338
+
339
+ /* Spinner */
340
+ [data-testid="stSpinner"] p { color: #475569 !important; }
341
+ </style>
342
+ """, unsafe_allow_html=True)
343
+
344
+ # ─────────────────────────────────────────────
345
+ # Load model
346
+ # ─────────────────────────────────────────────
347
+ @st.cache_resource(show_spinner=False)
348
+ def load_artifacts():
349
+ model = joblib.load("fraud_detection_final_model.pkl")
350
+ tfidf = joblib.load("tfidf_vectorizer.pkl")
351
+ sec_tok = AutoTokenizer.from_pretrained("nlpaueb/sec-bert-base")
352
+ sec_mdl = AutoModel.from_pretrained("nlpaueb/sec-bert-base")
353
+ sec_mdl.eval()
354
+ for p in sec_mdl.parameters():
355
+ p.requires_grad = False
356
+ return model, tfidf, sec_tok, sec_mdl
357
+
358
+ # ─────────────────────────────────────────────
359
+ # Helpers
360
+ # ─────────────────────────────────────────────
361
+ FRAUD_KEYWORDS = [
362
+ "restatement", "going concern", "material weakness",
363
+ "internal control", "misstatement", "irregularities",
364
+ "investigation", "SEC inquiry", "revenue recognition",
365
+ "write-off", "impairment", "earnings manipulation",
366
+ "off-balance sheet", "undisclosed", "overstated"
367
+ ]
368
+
369
+ def clean_text(text):
370
+ text = str(text)
371
+ text = re.sub(r"<.*?>", " ", text)
372
+ text = re.sub(r"[^a-zA-Z\s]", " ", text)
373
+ return re.sub(r"\s+", " ", text).lower().strip()
374
+
375
+ def get_sec_bert_embedding(text, tokenizer, model, device="cpu"):
376
+ inputs = tokenizer(
377
+ [text], padding=True, truncation=True,
378
+ max_length=512, return_tensors="pt"
379
+ ).to(device)
380
+ with torch.no_grad():
381
+ out = model(**inputs)
382
+ return out.last_hidden_state[:, 0, :].cpu().numpy()
383
+
384
+ def build_hybrid(text, tfidf_vec, sec_tok, sec_mdl):
385
+ cleaned = clean_text(text)
386
+ tfidf_feat = tfidf_vec.transform([cleaned]).toarray()
387
+ norm_tfidf = tfidf_feat / (np.linalg.norm(tfidf_feat) + 1e-9)
388
+ sec_emb = get_sec_bert_embedding(cleaned, sec_tok, sec_mdl)
389
+ norm_sec = sec_emb / (np.linalg.norm(sec_emb) + 1e-9)
390
+ return np.hstack([norm_tfidf, norm_sec])
391
+
392
+ def detect_keywords(text):
393
+ text_lower = text.lower()
394
+ return [kw for kw in FRAUD_KEYWORDS if kw in text_lower]
395
+
396
+ def risk_tier(prob):
397
+ if prob < 0.40:
398
+ return "Low Risk", "low", "badge-low", "prob-low", "#4ade80"
399
+ elif prob < 0.75:
400
+ return "Moderate Risk", "mod", "badge-mod", "prob-mod", "#fb923c"
401
+ else:
402
+ return "High Risk", "high", "badge-high", "prob-high", "#e879f9"
403
+
404
+ # ─────────────────────────────────────────────
405
+ # NAV BAR
406
+ # ─────────────────────────────────────────────
407
+ st.markdown("""
408
+ <div class="nav-bar">
409
+ <div class="nav-logo">
410
+ <div class="nav-logo-dot"></div>
411
+ SEC Fraud Detector
412
+ </div>
413
+ <div class="nav-badge">RESEARCH Β· BOOTCAMP PROJECT</div>
414
+ </div>
415
+ """, unsafe_allow_html=True)
416
+
417
+ # ─────────────────────────────────────────────
418
+ # HERO
419
+ # ─────────────────────────────────────────────
420
+ st.markdown("""
421
+ <div class="hero">
422
+ <div class="hero-eyebrow">NLP Β· Financial Statement Analysis</div>
423
+ <div class="hero-title">Detect fraud signals<br>in <span>SEC filings</span>.</div>
424
+ <div class="hero-desc">
425
+ Paste any financial statement excerpt β€” 10-K, 8-K, MD&A section β€”
426
+ and get an instant risk assessment powered by a hybrid
427
+ TF-IDF + SEC-BERT model trained on confirmed fraud cases.
428
+ </div>
429
+ <div class="stat-strip">
430
+ <div class="stat-cell">
431
+ <div class="stat-val">0.895</div>
432
+ <div class="stat-lbl">Test F1 Score</div>
433
+ </div>
434
+ <div class="stat-cell">
435
+ <div class="stat-val">1.000</div>
436
+ <div class="stat-lbl">Recall (fraud)</div>
437
+ </div>
438
+ <div class="stat-cell">
439
+ <div class="stat-val">0.983</div>
440
+ <div class="stat-lbl">ROC-AUC</div>
441
+ </div>
442
+ <div class="stat-cell">
443
+ <div class="stat-val">170</div>
444
+ <div class="stat-lbl">Training samples</div>
445
+ </div>
446
+ </div>
447
+ </div>
448
+ """, unsafe_allow_html=True)
449
+
450
+ # ─────────────────────────────────────────────
451
+ # MAIN LAYOUT β€” use st.columns to keep streamlit widgets working
452
+ # ─────────────────────────────────────────────
453
+ pad_l, col_input, gap, col_result, pad_r = st.columns([0.05, 1.2, 0.04, 0.85, 0.05])
454
+
455
+ # ─── INPUT COLUMN ───────────────────────────
456
+ with col_input:
457
+ st.markdown('<div class="card-title">FILING TEXT INPUT</div>', unsafe_allow_html=True)
458
+
459
+ sample_texts = {
460
+ "β€” paste your own text β€”": "",
461
+ "Sample: Fraud (going concern + restatement)":
462
+ "The Company has identified material weaknesses in its internal control over financial reporting. "
463
+ "Management has concluded that the previously issued financial statements contained misstatements "
464
+ "and will require restatement. There is substantial doubt about the Company's ability to continue "
465
+ "as a going concern. An SEC inquiry into revenue recognition practices is currently ongoing.",
466
+ "Sample: Non-fraud (clean filing)":
467
+ "Net revenues for the fiscal year ended December 31 were 4.2 billion dollars, "
468
+ "representing an increase of 8 percent compared to the prior year. "
469
+ "The increase was primarily driven by strong performance in our core product segments. "
470
+ "Operating income improved to 890 million dollars. "
471
+ "The Board of Directors approved a quarterly dividend of 0.42 dollars per share."
472
+ }
473
+
474
+ selected = st.selectbox("Load a sample or paste your own:", list(sample_texts.keys()),
475
+ label_visibility="collapsed")
476
+ default_text = sample_texts[selected]
477
+
478
+ user_input = st.text_area(
479
+ "Filing text",
480
+ value=default_text,
481
+ height=240,
482
+ placeholder="Paste SEC filing text here β€” MD&A, 10-K, 8-K, footnotes...",
483
+ label_visibility="collapsed"
484
+ )
485
+
486
+ analyze = st.button("Run Fraud Assessment β†’")
487
+
488
+ # Keyword scanner (always visible)
489
+ if user_input.strip():
490
+ hits = detect_keywords(user_input)
491
+ st.markdown('<div style="margin-top:20px">', unsafe_allow_html=True)
492
+ st.markdown('<div class="card-title" style="margin-bottom:10px">KEYWORD SCAN</div>',
493
+ unsafe_allow_html=True)
494
+ chips = ""
495
+ for kw in FRAUD_KEYWORDS:
496
+ cls = "kw-chip hit" if kw in hits else "kw-chip"
497
+ chips += f'<span class="{cls}">{kw}</span>'
498
+ st.markdown(f'<div class="kw-list">{chips}</div>', unsafe_allow_html=True)
499
+ if hits:
500
+ st.markdown(
501
+ f'<div style="margin-top:10px;font-size:12px;color:#c084fc">'
502
+ f'⚠ {len(hits)} fraud-associated term{"s" if len(hits)>1 else ""} detected</div>',
503
+ unsafe_allow_html=True)
504
+ st.markdown('</div>', unsafe_allow_html=True)
505
+
506
+ # ─── RESULT COLUMN ──────────────────────────
507
+ with col_result:
508
+ st.markdown('<div class="card-title">RISK ASSESSMENT</div>', unsafe_allow_html=True)
509
+
510
+ if not analyze and not user_input.strip():
511
+ st.markdown("""
512
+ <div style="padding:40px 0;text-align:center;color:#1e3a5f">
513
+ <div style="font-size:40px;margin-bottom:12px">πŸ”</div>
514
+ <div style="font-size:13px;color:#334155;line-height:1.7">
515
+ Enter filing text and click<br><strong style="color:#3b82f6">Run Fraud Assessment</strong>
516
+ </div>
517
+ </div>
518
+ """, unsafe_allow_html=True)
519
+
520
+ elif analyze:
521
+ if not user_input.strip():
522
+ st.markdown('<div style="color:#fb923c;font-size:13px;padding:12px 0">⚠ Please enter some filing text first.</div>',
523
+ unsafe_allow_html=True)
524
+ else:
525
+ with st.spinner("Running hybrid model inference..."):
526
+ try:
527
+ clf, tfidf_vec, sec_tok, sec_mdl = load_artifacts()
528
+ X = build_hybrid(user_input, tfidf_vec, sec_tok, sec_mdl)
529
+ prob = float(clf.predict_proba(X)[0][1])
530
+ label, tier, badge_cls, prob_cls, bar_color = risk_tier(prob)
531
+ hits = detect_keywords(user_input)
532
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
533
+
534
+ st.markdown(f'<div class="result-risk-badge {badge_cls}">{label}</div>',
535
+ unsafe_allow_html=True)
536
+
537
+ st.markdown(
538
+ f'<div class="prob-number {prob_cls}">{prob:.1%}</div>'
539
+ f'<div style="font-size:12px;color:#475569">fraud probability</div>',
540
+ unsafe_allow_html=True
541
+ )
542
+
543
+ fill_w = int(prob * 100)
544
+ st.markdown(
545
+ f'<div class="prob-bar-track">'
546
+ f'<div class="prob-bar-fill" style="width:{fill_w}%;background:{bar_color}"></div>'
547
+ f'</div>',
548
+ unsafe_allow_html=True
549
+ )
550
+
551
+ # Metrics
552
+ verdict = "Fraudulent pattern detected" if prob >= 0.70 else "No fraud pattern detected"
553
+ st.markdown(f"""
554
+ <div class="metric-row">
555
+ <span class="metric-key">verdict</span>
556
+ <span class="metric-val">{verdict}</span>
557
+ </div>
558
+ <div class="metric-row">
559
+ <span class="metric-key">fraud_prob</span>
560
+ <span class="metric-val-accent">{prob:.4f}</span>
561
+ </div>
562
+ <div class="metric-row">
563
+ <span class="metric-key">keywords_hit</span>
564
+ <span class="metric-val">{len(hits)} / {len(FRAUD_KEYWORDS)}</span>
565
+ </div>
566
+ <div class="metric-row">
567
+ <span class="metric-key">threshold</span>
568
+ <span class="metric-val">0.70</span>
569
+ </div>
570
+ <div class="metric-row">
571
+ <span class="metric-key">timestamp</span>
572
+ <span class="metric-val">{ts}</span>
573
+ </div>
574
+ """, unsafe_allow_html=True)
575
+
576
+ # Contextual guidance
577
+ guidance = {
578
+ "low": ("βœ“", "#4ade80", "Low lexical fraud risk. Filing language is consistent with non-fraudulent SEC disclosures."),
579
+ "mod": ("!", "#fb923c", "Moderate risk signals detected. One or more linguistic patterns align with fraud-adjacent language. Manual review recommended."),
580
+ "high": ("⚠", "#e879f9", "High-confidence fraud indicators detected. Filing language strongly matches confirmed fraud cases in the training corpus. Escalate for review."),
581
+ }
582
+ icon, color, msg = guidance[tier]
583
+ st.markdown(
584
+ f'<div style="margin-top:20px;padding:14px 16px;border-radius:8px;'
585
+ f'background:{color}10;border:1px solid {color}30;'
586
+ f'font-size:12px;color:{color};line-height:1.6">'
587
+ f'<strong>{icon} {label}:</strong> {msg}</div>',
588
+ unsafe_allow_html=True
589
+ )
590
+
591
+ except FileNotFoundError as e:
592
+ st.markdown(f"""
593
+ <div style="padding:20px;background:#1a0a0a;border:1px solid #7f1d1d;
594
+ border-radius:10px;font-size:13px;color:#fca5a5;line-height:1.7">
595
+ <strong>Model files not found.</strong><br>
596
+ Make sure these files are in the same directory as app.py:<br>
597
+ <code style="color:#f87171">fraud_detection_final_model.pkl</code><br>
598
+ <code style="color:#f87171">tfidf_vectorizer.pkl</code>
599
+ </div>
600
+ """, unsafe_allow_html=True)
601
+
602
+ # Model info card β€” always shown
603
+ st.markdown("""
604
+ <div style="margin-top:28px">
605
+ <div class="card-title">MODEL DETAILS</div>
606
+ <div class="info-row">
607
+ <span class="info-key">Architecture</span>
608
+ <span class="info-val">Logistic Regression</span>
609
+ </div>
610
+ <div class="info-row">
611
+ <span class="info-key">Embedding</span>
612
+ <span class="info-val">Hybrid TF-IDF (500) + SEC-BERT (768)</span>
613
+ </div>
614
+ <div class="info-row">
615
+ <span class="info-key">SEC-BERT</span>
616
+ <span class="info-val">nlpaueb/sec-bert-base Β· frozen</span>
617
+ </div>
618
+ <div class="info-row">
619
+ <span class="info-key">Dataset</span>
620
+ <span class="info-val">AmitKedia Β· 170 SEC filings<br>85 fraud / 85 non-fraud</span>
621
+ </div>
622
+ <div class="info-row">
623
+ <span class="info-key">Validation</span>
624
+ <span class="info-val">Stratified 5-fold CV</span>
625
+ </div>
626
+ <div class="info-row">
627
+ <span class="info-key">CV F1</span>
628
+ <span class="info-val">0.856 Β± 0.056</span>
629
+ </div>
630
+ </div>
631
+ """, unsafe_allow_html=True)
632
+
633
+ # ─────────────────────────────────────────────
634
+ # NOTICE
635
+ # ─────────────────────────────────────────────
636
+ st.markdown("""
637
+ <div class="notice">
638
+ <span>⚠ Research scope:</span>
639
+ This tool classifies filing text based on linguistic patterns learned from 170 SEC filings.
640
+ It does not perform forensic accounting analysis or evaluate structured financial data.
641
+ Results are probabilistic and should not replace professional audit judgment.
642
+ Built as a data science bootcamp capstone project.
643
+ </div>
644
+ """, unsafe_allow_html=True)
fraud_detection_final_model.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cae083729eabfd57d72fcbb152411ccf631d15e211e2fa7ec442ef40b012381c
3
+ size 11023
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ streamlit
2
+ scikit-learn
3
+ transformers
4
+ torch
5
+ numpy
6
+ pandas
tfidf_vectorizer.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dca5de5761fcc3f57b99245ee9e1c529c5a6e951bbe97bd0769311859a7d7501
3
+ size 20212