import streamlit as st
import pickle
import joblib
import torch
import numpy as np
import re
import os
from transformers import AutoTokenizer, AutoModel
from datetime import datetime
# ─────────────────────────────────────────────
# Page config
# ─────────────────────────────────────────────
st.set_page_config(
page_title="SEC Fraud Detector",
page_icon="🔍",
layout="wide",
initial_sidebar_state="collapsed"
)
# ─────────────────────────────────────────────
# CSS
# ─────────────────────────────────────────────
st.markdown("""
""", unsafe_allow_html=True)
# ─────────────────────────────────────────────
# Load model
# ─────────────────────────────────────────────
@st.cache_resource(show_spinner=False)
def load_artifacts():
model = joblib.load("fraud_detection_final_model.pkl")
tfidf = joblib.load("tfidf_vectorizer.pkl")
sec_tok = AutoTokenizer.from_pretrained("nlpaueb/sec-bert-base")
sec_mdl = AutoModel.from_pretrained("nlpaueb/sec-bert-base")
sec_mdl.eval()
for p in sec_mdl.parameters():
p.requires_grad = False
return model, tfidf, sec_tok, sec_mdl
# ─────────────────────────────────────────────
# Helpers
# ─────────────────────────────────────────────
FRAUD_KEYWORDS = [
"restatement", "going concern", "material weakness",
"internal control", "misstatement", "irregularities",
"investigation", "SEC inquiry", "revenue recognition",
"write-off", "impairment", "earnings manipulation",
"off-balance sheet", "undisclosed", "overstated"
]
def clean_text(text):
text = str(text)
text = re.sub(r"<.*?>", " ", text)
text = re.sub(r"[^a-zA-Z\s]", " ", text)
return re.sub(r"\s+", " ", text).lower().strip()
def get_sec_bert_embedding(text, tokenizer, model, device="cpu"):
inputs = tokenizer(
[text], padding=True, truncation=True,
max_length=512, return_tensors="pt"
).to(device)
with torch.no_grad():
out = model(**inputs)
return out.last_hidden_state[:, 0, :].cpu().numpy()
def build_hybrid(text, tfidf_vec, sec_tok, sec_mdl):
cleaned = clean_text(text)
tfidf_feat = tfidf_vec.transform([cleaned]).toarray()
norm_tfidf = tfidf_feat / (np.linalg.norm(tfidf_feat) + 1e-9)
sec_emb = get_sec_bert_embedding(cleaned, sec_tok, sec_mdl)
norm_sec = sec_emb / (np.linalg.norm(sec_emb) + 1e-9)
return np.hstack([norm_tfidf, norm_sec])
def detect_keywords(text):
text_lower = text.lower()
return [kw for kw in FRAUD_KEYWORDS if kw in text_lower]
def risk_tier(prob):
if prob < 0.40:
return "Low Risk", "low", "badge-low", "prob-low", "#4ade80"
elif prob < 0.75:
return "Moderate Risk", "mod", "badge-mod", "prob-mod", "#fb923c"
else:
return "High Risk", "high", "badge-high", "prob-high", "#e879f9"
# ─────────────────────────────────────────────
# NAV BAR
# ─────────────────────────────────────────────
st.markdown("""
RESEARCH · BOOTCAMP PROJECT
""", unsafe_allow_html=True)
# ─────────────────────────────────────────────
# HERO
# ─────────────────────────────────────────────
st.markdown("""
NLP · Financial Statement Analysis
Detect fraud signals
in SEC filings.
Paste any financial statement excerpt — 10-K, 8-K, MD&A section —
and get an instant risk assessment powered by a hybrid
TF-IDF + SEC-BERT model trained on confirmed fraud cases.
""", unsafe_allow_html=True)
# ─────────────────────────────────────────────
# MAIN LAYOUT — use st.columns to keep streamlit widgets working
# ─────────────────────────────────────────────
pad_l, col_input, gap, col_result, pad_r = st.columns([0.05, 1.2, 0.04, 0.85, 0.05])
# ─── INPUT COLUMN ───────────────────────────
with col_input:
st.markdown('FILING TEXT INPUT
', unsafe_allow_html=True)
sample_texts = {
"— paste your own text —": "",
"Sample: Fraud (going concern + restatement)":
"The Company has identified material weaknesses in its internal control over financial reporting. "
"Management has concluded that the previously issued financial statements contained misstatements "
"and will require restatement. There is substantial doubt about the Company's ability to continue "
"as a going concern. An SEC inquiry into revenue recognition practices is currently ongoing.",
"Sample: Non-fraud (clean filing)":
"Net revenues for the fiscal year ended December 31 were 4.2 billion dollars, "
"representing an increase of 8 percent compared to the prior year. "
"The increase was primarily driven by strong performance in our core product segments. "
"Operating income improved to 890 million dollars. "
"The Board of Directors approved a quarterly dividend of 0.42 dollars per share."
}
selected = st.selectbox("Load a sample or paste your own:", list(sample_texts.keys()),
label_visibility="collapsed")
default_text = sample_texts[selected]
user_input = st.text_area(
"Filing text",
value=default_text,
height=240,
placeholder="Paste SEC filing text here — MD&A, 10-K, 8-K, footnotes...",
label_visibility="collapsed"
)
analyze = st.button("Run Fraud Assessment →")
# Keyword scanner (always visible)
if user_input.strip():
hits = detect_keywords(user_input)
st.markdown('', unsafe_allow_html=True)
st.markdown('
KEYWORD SCAN
',
unsafe_allow_html=True)
chips = ""
for kw in FRAUD_KEYWORDS:
cls = "kw-chip hit" if kw in hits else "kw-chip"
chips += f'
{kw}'
st.markdown(f'
{chips}
', unsafe_allow_html=True)
if hits:
st.markdown(
f'
'
f'⚠ {len(hits)} fraud-associated term{"s" if len(hits)>1 else ""} detected
',
unsafe_allow_html=True)
st.markdown('
', unsafe_allow_html=True)
# ─── RESULT COLUMN ──────────────────────────
with col_result:
st.markdown('RISK ASSESSMENT
', unsafe_allow_html=True)
if not analyze and not user_input.strip():
st.markdown("""
🔍
Enter filing text and click
Run Fraud Assessment
""", unsafe_allow_html=True)
elif analyze:
if not user_input.strip():
st.markdown('⚠ Please enter some filing text first.
',
unsafe_allow_html=True)
else:
with st.spinner("Running hybrid model inference..."):
try:
clf, tfidf_vec, sec_tok, sec_mdl = load_artifacts()
X = build_hybrid(user_input, tfidf_vec, sec_tok, sec_mdl)
prob = float(clf.predict_proba(X)[0][1])
label, tier, badge_cls, prob_cls, bar_color = risk_tier(prob)
hits = detect_keywords(user_input)
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
st.markdown(f'{label}
',
unsafe_allow_html=True)
st.markdown(
f'{prob:.1%}
'
f'fraud probability
',
unsafe_allow_html=True
)
fill_w = int(prob * 100)
st.markdown(
f'',
unsafe_allow_html=True
)
# Metrics
verdict = "Fraudulent pattern detected" if prob >= 0.70 else "No fraud pattern detected"
st.markdown(f"""
verdict
{verdict}
fraud_prob
{prob:.4f}
keywords_hit
{len(hits)} / {len(FRAUD_KEYWORDS)}
threshold
0.70
timestamp
{ts}
""", unsafe_allow_html=True)
# Contextual guidance
guidance = {
"low": ("✓", "#4ade80", "Low lexical fraud risk. Filing language is consistent with non-fraudulent SEC disclosures."),
"mod": ("!", "#fb923c", "Moderate risk signals detected. One or more linguistic patterns align with fraud-adjacent language. Manual review recommended."),
"high": ("⚠", "#e879f9", "High-confidence fraud indicators detected. Filing language strongly matches confirmed fraud cases in the training corpus. Escalate for review."),
}
icon, color, msg = guidance[tier]
st.markdown(
f''
f'{icon} {label}: {msg}
',
unsafe_allow_html=True
)
except FileNotFoundError as e:
st.markdown(f"""
Model files not found.
Make sure these files are in the same directory as app.py:
fraud_detection_final_model.pkl
tfidf_vectorizer.pkl
""", unsafe_allow_html=True)
# Model info card — always shown
st.markdown("""
MODEL DETAILS
Architecture
Logistic Regression
Embedding
Hybrid TF-IDF (500) + SEC-BERT (768)
SEC-BERT
nlpaueb/sec-bert-base · frozen
Dataset
AmitKedia · 170 SEC filings
85 fraud / 85 non-fraud
Validation
Stratified 5-fold CV
CV F1
0.856 ± 0.056
""", unsafe_allow_html=True)
# ─────────────────────────────────────────────
# NOTICE
# ─────────────────────────────────────────────
st.markdown("""
⚠ Research scope:
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 data.
Results are probabilistic and should not replace professional audit judgment.
Built as a data science bootcamp capstone project.
""", unsafe_allow_html=True)