iremrit commited on
Commit
a29f5a7
·
verified ·
1 Parent(s): 24409e1

Upload 15 files

Browse files
__pycache__/config.cpython-312.pyc ADDED
Binary file (2.04 kB). View file
 
__pycache__/config_blackbox.cpython-312.pyc ADDED
Binary file (2.08 kB). View file
 
__pycache__/inference.cpython-312.pyc ADDED
Binary file (9.93 kB). View file
 
__pycache__/pipeline.cpython-312.pyc ADDED
Binary file (8.2 kB). View file
 
models/features.json ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "all_features": [
3
+ "Age",
4
+ "Num_Bank_Accounts",
5
+ "Num_Credit_Card",
6
+ "Delay_from_due_date",
7
+ "Num_of_Delayed_Payment",
8
+ "Num_of_Loan",
9
+ "Num_Credit_Inquiries",
10
+ "Monthly_Inhand_Salary",
11
+ "Interest_Rate",
12
+ "Outstanding_Debt",
13
+ "Credit_Utilization_Ratio",
14
+ "Monthly_Balance",
15
+ "Total_EMI_per_month",
16
+ "Amount_invested_monthly",
17
+ "Installment_to_Income",
18
+ "Delayed_Per_Loan",
19
+ "Debt_to_Income_Ratio",
20
+ "DTI_x_LoanCount",
21
+ "Debt_Per_Loan",
22
+ "Loan_Count_Calculated",
23
+ "Loan_Auto_Loan",
24
+ "Loan_Credit-Builder_Loan",
25
+ "Loan_Personal_Loan",
26
+ "Loan_Home_Equity_Loan",
27
+ "Loan_Mortgage_Loan",
28
+ "Loan_Student_Loan",
29
+ "Loan_Debt_Consolidation_Loan",
30
+ "Loan_Payday_Loan",
31
+ "Log_Annual_Income",
32
+ "Credit_Mix_Ordinal",
33
+ "Occupation_Architect",
34
+ "Occupation_Developer",
35
+ "Occupation_Doctor",
36
+ "Occupation_Engineer",
37
+ "Occupation_Entrepreneur",
38
+ "Occupation_Journalist",
39
+ "Occupation_Lawyer",
40
+ "Occupation_Manager",
41
+ "Occupation_Mechanic",
42
+ "Occupation_Media_Manager",
43
+ "Occupation_Musician",
44
+ "Occupation_Scientist",
45
+ "Occupation_Teacher",
46
+ "Occupation_Writer",
47
+ "Occupation________",
48
+ "Payment_of_Min_Amount_No",
49
+ "Payment_of_Min_Amount_Yes",
50
+ "Payment_Behaviour_High_spent_Medium_value_payments",
51
+ "Payment_Behaviour_High_spent_Small_value_payments",
52
+ "Payment_Behaviour_Low_spent_Large_value_payments",
53
+ "Payment_Behaviour_Low_spent_Medium_value_payments",
54
+ "Payment_Behaviour_Low_spent_Small_value_payments",
55
+ "Payment_Behaviour_Unknown"
56
+ ],
57
+ "top_10_features": [
58
+ "Credit_Mix_Ordinal",
59
+ "Outstanding_Debt",
60
+ "Delay_from_due_date",
61
+ "Payment_of_Min_Amount_Yes",
62
+ "Num_Credit_Card",
63
+ "Interest_Rate",
64
+ "Num_of_Delayed_Payment",
65
+ "Installment_to_Income",
66
+ "Num_Bank_Accounts",
67
+ "Num_Credit_Inquiries"
68
+ ]
69
+ }
models/final_model.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ca63338de468e64e8a33c22eb98663577040adb5eb014c25cbe8a62ccdb173ba
3
+ size 37459067
templates/index.html ADDED
@@ -0,0 +1,442 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>FinRisk-AI</title>
7
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>💳</text></svg>">
8
+ <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap" rel="stylesheet">
9
+ <style>
10
+ * {
11
+ margin: 0;
12
+ padding: 0;
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ body {
17
+ font-family: 'Montserrat', sans-serif;
18
+ background: linear-gradient(135deg, #0f4c75 0%, #3282b8 25%, #bbe1fa 50%, #1b262c 75%, #0f4c75 100%);
19
+ background-size: 400% 400%;
20
+ animation: gradientShift 15s ease infinite;
21
+ min-height: 100vh;
22
+ padding: 20px;
23
+ position: relative;
24
+ overflow: hidden;
25
+ }
26
+
27
+ body::before {
28
+ content: '';
29
+ position: absolute;
30
+ top: 0;
31
+ left: 0;
32
+ right: 0;
33
+ bottom: 0;
34
+ background: radial-gradient(circle at 20% 80%, rgba(0, 128, 0, 0.1) 0%, transparent 50%),
35
+ radial-gradient(circle at 80% 20%, rgba(255, 69, 0, 0.1) 0%, transparent 50%),
36
+ radial-gradient(circle at 40% 40%, rgba(0, 139, 139, 0.1) 0%, transparent 50%);
37
+ animation: float 20s ease-in-out infinite;
38
+ pointer-events: none;
39
+ }
40
+
41
+ @keyframes gradientShift {
42
+ 0% { background-position: 0% 50%; }
43
+ 50% { background-position: 100% 50%; }
44
+ 100% { background-position: 0% 50%; }
45
+ }
46
+
47
+ @keyframes float {
48
+ 0%, 100% { transform: translateY(0px) rotate(0deg); }
49
+ 33% { transform: translateY(-10px) rotate(1deg); }
50
+ 66% { transform: translateY(10px) rotate(-1deg); }
51
+ }
52
+
53
+ .container {
54
+ max-width: 1200px;
55
+ margin: 0 auto;
56
+ }
57
+
58
+ .header {
59
+ text-align: center;
60
+ color: white;
61
+ margin-bottom: 30px;
62
+ }
63
+
64
+ .header h1 {
65
+ font-size: 2.8rem;
66
+ margin-bottom: 10px;
67
+ font-weight: 700;
68
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
69
+ }
70
+
71
+ .header p {
72
+ font-size: 1.2rem;
73
+ opacity: 0.9;
74
+ font-weight: 300;
75
+ }
76
+
77
+ .card {
78
+ background: white;
79
+ border-radius: 15px;
80
+ padding: 30px;
81
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
82
+ margin-bottom: 20px;
83
+ }
84
+
85
+ .form-grid {
86
+ display: grid;
87
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
88
+ gap: 15px;
89
+ margin-bottom: 20px;
90
+ }
91
+
92
+ .form-group {
93
+ display: flex;
94
+ flex-direction: column;
95
+ }
96
+
97
+ .form-group label {
98
+ font-size: 0.85rem;
99
+ font-weight: 600;
100
+ margin-bottom: 5px;
101
+ color: #34495e;
102
+ }
103
+
104
+ .form-group input {
105
+ padding: 10px;
106
+ border: 2px solid #e0e0e0;
107
+ border-radius: 8px;
108
+ font-size: 0.95rem;
109
+ transition: border-color 0.3s;
110
+ }
111
+
112
+ .form-group input:focus {
113
+ outline: none;
114
+ border-color: #667eea;
115
+ }
116
+
117
+ .button-group {
118
+ display: flex;
119
+ gap: 10px;
120
+ justify-content: center;
121
+ }
122
+
123
+ .btn {
124
+ padding: 12px 30px;
125
+ border: none;
126
+ border-radius: 8px;
127
+ font-size: 1rem;
128
+ font-weight: 600;
129
+ cursor: pointer;
130
+ transition: all 0.3s;
131
+ }
132
+
133
+ .btn-primary {
134
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
135
+ color: white;
136
+ }
137
+
138
+ .btn-primary:hover {
139
+ transform: translateY(-2px);
140
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
141
+ }
142
+
143
+ .btn:disabled {
144
+ opacity: 0.5;
145
+ cursor: not-allowed;
146
+ transform: none;
147
+ }
148
+
149
+ .btn-secondary {
150
+ background: #f0f0f0;
151
+ color: #34495e;
152
+ }
153
+
154
+ .btn-secondary:hover {
155
+ background: #e0e0e0;
156
+ }
157
+
158
+ .result {
159
+ display: none;
160
+ padding: 20px;
161
+ border-radius: 10px;
162
+ margin-top: 20px;
163
+ }
164
+
165
+ .result.show {
166
+ display: block;
167
+ animation: fadeIn 0.5s;
168
+ }
169
+
170
+ @keyframes fadeIn {
171
+ from { opacity: 0; transform: translateY(-10px); }
172
+ to { opacity: 1; transform: translateY(0); }
173
+ }
174
+
175
+ .result-low {
176
+ background: #d4edda;
177
+ border-left: 5px solid #28a745;
178
+ }
179
+
180
+ .result-medium {
181
+ background: #fff3cd;
182
+ border-left: 5px solid #ffc107;
183
+ }
184
+
185
+ .result-high {
186
+ background: #f8d7da;
187
+ border-left: 5px solid #dc3545;
188
+ }
189
+
190
+ .result h3 {
191
+ margin-bottom: 10px;
192
+ font-size: 1.3rem;
193
+ }
194
+
195
+ .result-details {
196
+ display: grid;
197
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
198
+ gap: 15px;
199
+ margin-top: 15px;
200
+ }
201
+
202
+ .result-item {
203
+ padding: 10px;
204
+ background: rgba(255,255,255,0.5);
205
+ border-radius: 5px;
206
+ }
207
+
208
+ .result-item strong {
209
+ display: block;
210
+ margin-bottom: 5px;
211
+ font-size: 0.9rem;
212
+ }
213
+
214
+ .result-item span {
215
+ font-size: 1.2rem;
216
+ font-weight: 600;
217
+ }
218
+
219
+ .loading {
220
+ display: none;
221
+ text-align: center;
222
+ padding: 20px;
223
+ }
224
+
225
+ .loading.show {
226
+ display: block;
227
+ }
228
+
229
+ .spinner {
230
+ border: 4px solid #f3f3f3;
231
+ border-top: 4px solid #667eea;
232
+ border-radius: 50%;
233
+ width: 40px;
234
+ height: 40px;
235
+ animation: spin 1s linear infinite;
236
+ margin: 0 auto;
237
+ }
238
+
239
+ @keyframes spin {
240
+ 0% { transform: rotate(0deg); }
241
+ 100% { transform: rotate(360deg); }
242
+ }
243
+
244
+ .footer {
245
+ text-align: center;
246
+ color: white;
247
+ margin-top: 30px;
248
+ opacity: 0.8;
249
+ font-weight: 300;
250
+ }
251
+ </style>
252
+ </head>
253
+ <body>
254
+ <div class="container">
255
+ <div class="header">
256
+ <h1>💳 FinRisk-AI</h1>
257
+ <p>Intelligent Credit Risk Analysis Powered by AI</p>
258
+ </div>
259
+
260
+ <div class="card">
261
+ <form id="predictionForm">
262
+ <div class="form-grid" id="featureInputs"></div>
263
+
264
+ <div class="button-group">
265
+ <button type="submit" class="btn btn-primary">Predict Risk</button>
266
+ <button type="button" class="btn btn-secondary" onclick="fillSampleData()">Fill Sample Data</button>
267
+ <button type="reset" class="btn btn-secondary">Clear Form</button>
268
+ </div>
269
+
270
+ </form>
271
+
272
+ <div class="loading" id="loading">
273
+ <div class="spinner"></div>
274
+ <p style="margin-top: 10px;">Calculating risk...</p>
275
+ </div>
276
+
277
+ <div class="result" id="result"></div>
278
+ </div>
279
+
280
+ <div class="footer">
281
+ <p>FinRisk-AI v1.0 | 50+ Features | Stacking Classifier</p>
282
+ </div>
283
+ </div>
284
+
285
+ <script>
286
+ const features = {{ features | tojson }};
287
+
288
+ function createFeatureInputs() {
289
+ const container = document.getElementById('featureInputs');
290
+ if (features && features.top_10_features) {
291
+ features.top_10_features.forEach(feature => {
292
+ const div = document.createElement('div');
293
+ div.className = 'form-group';
294
+ div.innerHTML = `
295
+ <label for="${feature}">${feature}</label>
296
+ <input type="number" step="any" id="${feature}" name="${feature}" required>
297
+ `;
298
+ container.appendChild(div);
299
+ });
300
+ }
301
+ }
302
+
303
+ function fillSampleData() {
304
+ const sampleData = {
305
+ 'Credit_Mix_Ordinal': 2,
306
+ 'Outstanding_Debt': 15000,
307
+ 'Delay_from_due_date': 5,
308
+ 'Payment_of_Min_Amount_Yes': 1,
309
+ 'Num_Credit_Card': 3,
310
+ 'Interest_Rate': 12,
311
+ 'Num_of_Delayed_Payment': 2,
312
+ 'Installment_to_Income': 0.25,
313
+ 'Num_Bank_Accounts': 4,
314
+ 'Num_Credit_Inquiries': 1
315
+ };
316
+
317
+ features.top_10_features.forEach(feature => {
318
+ const input = document.getElementById(feature);
319
+ input.value = sampleData[feature] || 0;
320
+ });
321
+ }
322
+
323
+ document.getElementById('predictionForm').addEventListener('submit', async (e) => {
324
+ e.preventDefault();
325
+
326
+ const formData = new FormData(e.target);
327
+ const featuresData = {};
328
+
329
+ // Include all features, using form values for top_10_features and defaults for others
330
+ features.all_features.forEach(feature => {
331
+ if (features.top_10_features.includes(feature)) {
332
+ featuresData[feature] = parseFloat(formData.get(feature));
333
+ } else {
334
+ featuresData[feature] = 0; // Default value for features not in form
335
+ }
336
+ });
337
+
338
+ document.getElementById('loading').classList.add('show');
339
+ document.getElementById('result').classList.remove('show');
340
+
341
+ try {
342
+ const response = await fetch('/predict', {
343
+ method: 'POST',
344
+ headers: {
345
+ 'Content-Type': 'application/json',
346
+ },
347
+ body: JSON.stringify({ features: featuresData })
348
+ });
349
+
350
+ const data = await response.json();
351
+ displayResult(data);
352
+ } catch (error) {
353
+ alert('Error: ' + error.message);
354
+ } finally {
355
+ document.getElementById('loading').classList.remove('show');
356
+ }
357
+ });
358
+
359
+ function displayResult(data) {
360
+ const resultDiv = document.getElementById('result');
361
+
362
+ // Map prediction to risk level for styling
363
+ const riskLevel = data.prediction.toLowerCase() === 'poor' ? 'high' :
364
+ data.prediction.toLowerCase() === 'standard' ? 'medium' : 'low';
365
+
366
+ // Create message based on prediction
367
+ const message = `Your credit score is predicted to be: ${data.prediction}`;
368
+
369
+ resultDiv.className = `result result-${riskLevel} show`;
370
+ resultDiv.innerHTML = `
371
+ <h3>${message}</h3>
372
+ <div class="result-details">
373
+ <div class="result-item">
374
+ <strong>Credit Score</strong>
375
+ <span style="text-transform: uppercase;">${data.prediction}</span>
376
+ </div>
377
+ <div class="result-item">
378
+ <strong>Features Analyzed</strong>
379
+ <span>${data.features_used}</span>
380
+ </div>
381
+ </div>
382
+ `;
383
+ }
384
+
385
+ createFeatureInputs();
386
+
387
+ // Enable Calculator API button when all inputs are filled
388
+ const inputs = document.querySelectorAll('#featureInputs input');
389
+ const calculatorBtn = document.getElementById('calculatorBtn');
390
+
391
+ function checkInputs() {
392
+ let allFilled = true;
393
+ inputs.forEach(input => {
394
+ if (!input.value.trim()) {
395
+ allFilled = false;
396
+ }
397
+ });
398
+ calculatorBtn.disabled = !allFilled;
399
+ }
400
+
401
+ inputs.forEach(input => {
402
+ input.addEventListener('input', checkInputs);
403
+ });
404
+
405
+ calculatorBtn.addEventListener('click', async () => {
406
+ const featuresData = {};
407
+
408
+ // Include all features, using form values for top_10_features and defaults for others
409
+ if (features && features.all_features && features.top_10_features) {
410
+ features.all_features.forEach(feature => {
411
+ if (features.top_10_features.includes(feature)) {
412
+ const input = document.getElementById(feature);
413
+ featuresData[feature] = parseFloat(input.value);
414
+ } else {
415
+ featuresData[feature] = 0; // Default value for features not in form
416
+ }
417
+ });
418
+ }
419
+
420
+ document.getElementById('loading').classList.add('show');
421
+ document.getElementById('result').classList.remove('show');
422
+
423
+ try {
424
+ const response = await fetch('/predict', {
425
+ method: 'POST',
426
+ headers: {
427
+ 'Content-Type': 'application/json',
428
+ },
429
+ body: JSON.stringify({ features: featuresData })
430
+ });
431
+
432
+ const data = await response.json();
433
+ displayResult(data);
434
+ } catch (error) {
435
+ alert('Error: ' + error.message);
436
+ } finally {
437
+ document.getElementById('loading').classList.remove('show');
438
+ }
439
+ });
440
+ </script>
441
+ </body>
442
+ </html>
tests/__pycache__/config.cpython-312.pyc ADDED
Binary file (1.61 kB). View file
 
tests/__pycache__/inference.cpython-312.pyc ADDED
Binary file (3.53 kB). View file
 
tests/__pycache__/pipeline.cpython-312.pyc ADDED
Binary file (2.73 kB). View file
 
tests/_init_.py ADDED
@@ -0,0 +1 @@
 
 
1
+ #API KEY
tests/app.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from fastapi import FastAPI, HTTPException, Request
3
+ from fastapi.responses import HTMLResponse
4
+ from fastapi.templating import Jinja2Templates
5
+ from pydantic import BaseModel
6
+ from typing import Dict
7
+ from config import API_TITLE, API_VERSION, API_DESCRIPTION
8
+ from inference import predictor
9
+
10
+ app = FastAPI(title=API_TITLE, version=API_VERSION, description=API_DESCRIPTION)
11
+
12
+ templates = Jinja2Templates(directory="src/templates")
13
+
14
+
15
+ class PredictionRequest(BaseModel):
16
+ features: Dict[str, float]
17
+
18
+
19
+ class PredictionResponse(BaseModel):
20
+ prediction: str
21
+ features_used: int
22
+
23
+
24
+ class ProbabilityResponse(BaseModel):
25
+ probabilities: Dict[str, float]
26
+ features_used: int
27
+
28
+
29
+ @app.get("/", response_class=HTMLResponse)
30
+ async def home(request: Request):
31
+ feature_names = predictor.get_feature_names()
32
+ # Load top 10 features from features.json
33
+ predictor.load_model() # Ensure features are loaded
34
+ top_10_features = predictor.features['top_10_features']
35
+ return templates.TemplateResponse(
36
+ "index.html",
37
+ {"request": request, "features": {"all_features": feature_names, "top_10_features": top_10_features}}
38
+ )
39
+
40
+
41
+ @app.get("/health")
42
+ async def health():
43
+ return {
44
+ "status": "healthy",
45
+ "model_loaded": predictor._model_loaded,
46
+ "model_ready": predictor.model is not None
47
+ }
48
+
49
+
50
+ @app.get("/features")
51
+ async def get_features():
52
+ return {"features": predictor.get_feature_names()}
53
+
54
+
55
+ @app.post("/predict", response_model=PredictionResponse)
56
+ async def predict(request: PredictionRequest):
57
+
58
+ # Validate missing features
59
+ expected = set(predictor.get_feature_names())
60
+ incoming = set(request.features.keys())
61
+
62
+ missing = expected - incoming
63
+ if missing:
64
+ raise HTTPException(400, f"Missing features: {missing}")
65
+ prediction = predictor.predict(request.features)
66
+
67
+ return PredictionResponse(
68
+ prediction=prediction,
69
+ features_used=len(request.features)
70
+ )
71
+
72
+
73
+ @app.post("/predict_proba", response_model=ProbabilityResponse)
74
+ async def predict_proba(request: PredictionRequest):
75
+
76
+ # Validate missing features
77
+ expected = set(predictor.get_feature_names())
78
+ incoming = set(request.features.keys())
79
+
80
+ missing = expected - incoming
81
+ if missing:
82
+ raise HTTPException(400, f"Missing features: {missing}")
83
+ probabilities = predictor.predict_proba(request.features)
84
+
85
+ return ProbabilityResponse(
86
+ probabilities=probabilities,
87
+ features_used=len(request.features)
88
+ )
89
+
90
+
91
+ if __name__ == "__main__":
92
+ import uvicorn
93
+ port = int(os.environ.get("PORT", 8000))
94
+ uvicorn.run(app, host="0.0.0.0", port=port)
tests/config.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ # Paths
4
+ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
5
+
6
+ DATA_PATH = os.path.join(BASE_DIR, '..', 'data')
7
+ MODELS_PATH = os.path.join(BASE_DIR, 'models')
8
+
9
+ MODEL_FILENAME = 'final_model.pkl'
10
+ MODEL_PATH = os.path.join(MODELS_PATH, MODEL_FILENAME)
11
+
12
+ FEATURES_PATH = os.path.join(MODELS_PATH, 'features.json')
13
+
14
+
15
+ # API Configurations
16
+ API_TITLE = "FinRisk-AI API"
17
+ API_VERSION = "1.0.0"
18
+ API_DESCRIPTION = (
19
+ "Credit Score Classification service that predicts a customer's "
20
+ "credit category (Good, Standard, Poor). Built using a complete ML "
21
+ "pipeline and the system decided to utilize the model which uses an optimized "
22
+ "stacked ensemble (Random Forest + XGBoost + Logistic Regression) "
23
+ "achieving strong accuracy and robust generalization. Suitable for "
24
+ "automated underwriting and risk assessment."
25
+ )
26
+
27
+ # Risk levels and messages (placeholders)
28
+ RISK_LEVELS = {
29
+ 'low': (0.0, 0.3),
30
+ 'medium': (0.3, 0.7),
31
+ 'high': (0.7, 1.0)
32
+ }
33
+
34
+ RISK_MESSAGES = {
35
+ 'low': 'Low risk',
36
+ 'medium': 'Medium risk',
37
+ 'high': 'High risk'
38
+ }
tests/inference.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import joblib
3
+ import pandas as pd
4
+ from typing import Dict
5
+ from config import MODEL_PATH, FEATURES_PATH
6
+
7
+
8
+ class CreditScorePredictor:
9
+ def __init__(self):
10
+ self.model = None
11
+ self.features = None
12
+ self._model_loaded = False
13
+
14
+ def load_model(self):
15
+ if not self._model_loaded:
16
+ self.model = joblib.load(MODEL_PATH)
17
+ with open(FEATURES_PATH, 'r') as f:
18
+ self.features = json.load(f)
19
+ self._model_loaded = True
20
+
21
+ def predict(self, features_dict: Dict[str, float]) -> str:
22
+ # Ensure model is loaded
23
+ self.load_model()
24
+
25
+ df = pd.DataFrame([features_dict])
26
+
27
+ # Ensure correct feature order
28
+ df = df[
29
+ self.features['all_features']
30
+ ]
31
+
32
+ # Get prediction
33
+ pred_class = self.model.predict(df)[0]
34
+
35
+ # Map to credit score labels
36
+ credit_labels = {0: 'Poor', 1: 'Standard', 2: 'Good'}
37
+ prediction = credit_labels.get(pred_class, 'Unknown')
38
+
39
+ return prediction
40
+
41
+ def predict_proba(self, features_dict: Dict[str, float]) -> Dict[str, float]:
42
+ # Ensure model is loaded
43
+ self.load_model()
44
+
45
+ df = pd.DataFrame([features_dict])
46
+
47
+ # Ensure correct feature order
48
+ df = df[self.features['all_features']]
49
+
50
+ # Get prediction probabilities
51
+ proba = self.model.predict_proba(df)[0]
52
+
53
+ # Map to credit score labels
54
+ credit_labels = {0: 'Poor', 1: 'Standard', 2: 'Good'}
55
+ probabilities = {
56
+ credit_labels[i]: float(proba[i]) for i in range(len(proba))
57
+ }
58
+
59
+ return probabilities
60
+
61
+ def get_feature_names(self):
62
+ # Ensure model is loaded to get feature names
63
+ self.load_model()
64
+ return self.features['all_features']
65
+
66
+ def get_top_features(self, n=10):
67
+ # Ensure model is loaded
68
+ self.load_model()
69
+ # Top 10 most important features based on model evaluation
70
+ top_features = [
71
+ 'Credit_Mix_Ordinal',
72
+ 'Outstanding_Debt',
73
+ 'Delay_from_due_date',
74
+ 'Payment_of_Min_Amount_Yes',
75
+ 'Changed_Credit_Limit',
76
+ 'Credit_Utilization_Ratio',
77
+ 'Monthly_Balance',
78
+ 'Num_Bank_Accounts',
79
+ 'Num_Credit_Inquiries',
80
+ 'Annual_Income'
81
+ ]
82
+ return top_features[:n]
83
+
84
+
85
+ predictor = CreditScorePredictor()
tests/pipeline.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import pandas as pd
3
+ from sklearn.ensemble import RandomForestClassifier, StackingClassifier
4
+ from sklearn.linear_model import LogisticRegression
5
+ from xgboost import XGBClassifier
6
+ import joblib
7
+ import os
8
+ from config import DATA_PATH, MODELS_PATH, MODEL_FILENAME, FEATURES_PATH
9
+
10
+ # Top 10 features for user input
11
+ SELECTED_FEATURES = [
12
+ 'Credit_Mix_Ordinal',
13
+ 'Outstanding_Debt',
14
+ 'Delay_from_due_date',
15
+ 'Payment_of_Min_Amount_Yes',
16
+ 'Num_Credit_Card',
17
+ 'Interest_Rate',
18
+ 'Num_of_Delayed_Payment',
19
+ 'Installment_to_Income',
20
+ 'Num_Bank_Accounts',
21
+ 'Num_Credit_Inquiries'
22
+ ]
23
+
24
+ def run_pipeline():
25
+ print("Starting pipeline...")
26
+
27
+ # Load processed training data
28
+ train_processed_path = os.path.join(DATA_PATH, 'processed', 'train_processed.csv')
29
+ if not os.path.exists(train_processed_path):
30
+ raise FileNotFoundError(f"Processed training data not found at {train_processed_path}")
31
+
32
+ train_processed = pd.read_csv(train_processed_path)
33
+
34
+ # Train model with ALL FEATURES except the target
35
+ target = 'Credit_Score'
36
+ if target not in train_processed.columns:
37
+ raise ValueError("Target column 'Credit_Score' is missing from processed training data.")
38
+
39
+ X = train_processed.drop(target, axis=1)
40
+ y = train_processed[target]
41
+
42
+ ALL_FEATURES = X.columns.tolist()
43
+
44
+ print(f"Training model using ALL {len(ALL_FEATURES)} features...")
45
+ print(f"Training data loaded: {X.shape[0]} samples, {X.shape[1]} features")
46
+
47
+ # Define models
48
+ rf_model = RandomForestClassifier(
49
+ n_estimators=300,
50
+ max_depth=12,
51
+ class_weight='balanced',
52
+ criterion='entropy',
53
+ random_state=1907,
54
+ n_jobs=-1
55
+ )
56
+
57
+ xgb_model = XGBClassifier(
58
+ n_estimators=300,
59
+ learning_rate=0.1,
60
+ max_depth=6,
61
+ random_state=1907,
62
+ verbosity=0
63
+ )
64
+
65
+ stacking_clf = StackingClassifier(
66
+ estimators=[('rf', rf_model), ('xgb', xgb_model)],
67
+ final_estimator=LogisticRegression(max_iter=1000, random_state=1907),
68
+ cv=5
69
+ )
70
+
71
+ # Train model
72
+ print("Training stacking classifier...")
73
+ stacking_clf.fit(X, y)
74
+
75
+ # Save model
76
+ os.makedirs(MODELS_PATH, exist_ok=True)
77
+ model_path = os.path.join(MODELS_PATH, MODEL_FILENAME)
78
+ joblib.dump(stacking_clf, model_path)
79
+ print(f"Model saved to {model_path}")
80
+
81
+ # Save BOTH feature lists
82
+ feature_data = {
83
+ "all_features": ALL_FEATURES,
84
+ "top_10_features": SELECTED_FEATURES
85
+ }
86
+
87
+ with open(FEATURES_PATH, 'w') as f:
88
+ json.dump(feature_data, f, indent=4)
89
+
90
+ print("Feature lists saved.")
91
+ print("Pipeline completed successfully.")
92
+
93
+
94
+ if __name__ == "__main__":
95
+ run_pipeline()