Upload folder using huggingface_hub
Browse files- .claude/settings.local.json +3 -1
- Example_JSON/examples.md +286 -12
- api/predictor.py +10 -24
- tests/conftest.py +2 -2
- tests/unit/test_predictor.py +52 -72
.claude/settings.local.json
CHANGED
|
@@ -11,7 +11,9 @@
|
|
| 11 |
"Bash(git -C \"C:\\\\Users\\\\Kevin\\\\projects\\\\OC_P8\" diff)",
|
| 12 |
"Bash(git -C \"C:\\\\Users\\\\Kevin\\\\projects\\\\OC_P8\" log --oneline -10)",
|
| 13 |
"PowerShell(uv run ruff check api feature_engineering tests scripts)",
|
| 14 |
-
"PowerShell(uv run pytest --cov=api --cov-fail-under=80 -q)"
|
|
|
|
|
|
|
| 15 |
]
|
| 16 |
}
|
| 17 |
}
|
|
|
|
| 11 |
"Bash(git -C \"C:\\\\Users\\\\Kevin\\\\projects\\\\OC_P8\" diff)",
|
| 12 |
"Bash(git -C \"C:\\\\Users\\\\Kevin\\\\projects\\\\OC_P8\" log --oneline -10)",
|
| 13 |
"PowerShell(uv run ruff check api feature_engineering tests scripts)",
|
| 14 |
+
"PowerShell(uv run pytest --cov=api --cov-fail-under=80 -q)",
|
| 15 |
+
"Bash(ls \"C:/Users/Kevin/projects/OC_P6/notebooks/\" 2>&1)",
|
| 16 |
+
"Read(//c/Users/Kevin/projects/OC_P6/notebooks/**)"
|
| 17 |
]
|
| 18 |
}
|
| 19 |
}
|
Example_JSON/examples.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
| 1 |
# Exemples réalistes pour POST /predict
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
Toutes les valeurs respectent les bornes définies dans `api/schemas.py`.
|
| 5 |
|
| 6 |
---
|
|
@@ -13,7 +17,7 @@ ratio crédit/revenu modéré.
|
|
| 13 |
|
| 14 |
```json
|
| 15 |
{
|
| 16 |
-
"SK_ID_CURR":
|
| 17 |
"NAME_CONTRACT_TYPE": "Cash loans",
|
| 18 |
"CODE_GENDER": "F",
|
| 19 |
"FLAG_OWN_CAR": "Y",
|
|
@@ -53,9 +57,9 @@ ratio crédit/revenu modéré.
|
|
| 53 |
"REG_CITY_NOT_WORK_CITY": 0,
|
| 54 |
"LIVE_CITY_NOT_WORK_CITY": 0,
|
| 55 |
"ORGANIZATION_TYPE": "Bank",
|
| 56 |
-
"EXT_SOURCE_1": 0.
|
| 57 |
-
"EXT_SOURCE_2": 0.
|
| 58 |
-
"EXT_SOURCE_3": 0.
|
| 59 |
"APARTMENTS_AVG": 0.1242,
|
| 60 |
"BASEMENTAREA_AVG": 0.0876,
|
| 61 |
"YEARS_BEGINEXPLUATATION_AVG": 0.9821,
|
|
@@ -146,7 +150,7 @@ ancienneté 4 ans, locataire, EXT_SOURCE autour de 0.4-0.5, ratio crédit/revenu
|
|
| 146 |
|
| 147 |
```json
|
| 148 |
{
|
| 149 |
-
"SK_ID_CURR":
|
| 150 |
"NAME_CONTRACT_TYPE": "Cash loans",
|
| 151 |
"CODE_GENDER": "M",
|
| 152 |
"FLAG_OWN_CAR": "N",
|
|
@@ -187,8 +191,8 @@ ancienneté 4 ans, locataire, EXT_SOURCE autour de 0.4-0.5, ratio crédit/revenu
|
|
| 187 |
"LIVE_CITY_NOT_WORK_CITY": 1,
|
| 188 |
"ORGANIZATION_TYPE": "Industry: type 3",
|
| 189 |
"EXT_SOURCE_1": 0.42,
|
| 190 |
-
"EXT_SOURCE_2": 0.
|
| 191 |
-
"EXT_SOURCE_3": 0.
|
| 192 |
"APARTMENTS_AVG": null,
|
| 193 |
"BASEMENTAREA_AVG": null,
|
| 194 |
"YEARS_BEGINEXPLUATATION_AVG": null,
|
|
@@ -280,7 +284,7 @@ plusieurs requêtes au crédit bureau récentes, défauts sociaux observés.
|
|
| 280 |
|
| 281 |
```json
|
| 282 |
{
|
| 283 |
-
"SK_ID_CURR":
|
| 284 |
"NAME_CONTRACT_TYPE": "Cash loans",
|
| 285 |
"CODE_GENDER": "M",
|
| 286 |
"FLAG_OWN_CAR": "N",
|
|
@@ -320,9 +324,279 @@ plusieurs requêtes au crédit bureau récentes, défauts sociaux observés.
|
|
| 320 |
"REG_CITY_NOT_WORK_CITY": 1,
|
| 321 |
"LIVE_CITY_NOT_WORK_CITY": 1,
|
| 322 |
"ORGANIZATION_TYPE": "Construction",
|
| 323 |
-
"EXT_SOURCE_1": 0.
|
| 324 |
-
"EXT_SOURCE_2": 0.
|
| 325 |
-
"EXT_SOURCE_3": 0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
"APARTMENTS_AVG": null,
|
| 327 |
"BASEMENTAREA_AVG": null,
|
| 328 |
"YEARS_BEGINEXPLUATATION_AVG": null,
|
|
|
|
| 1 |
# Exemples réalistes pour POST /predict
|
| 2 |
|
| 3 |
+
Cinq profils-types couvrant le spectre de risque attendu par le modèle :
|
| 4 |
+
trois clients **connus** (SK_ID_CURR présent dans `features_store.parquet`,
|
| 5 |
+
donc historique bureau/prev/POS/CC/install agrégé) et deux clients
|
| 6 |
+
**inconnus** (SK_ID_CURR absent, le `no_history_template` neutralise tout
|
| 7 |
+
l'historique et le score ne repose plus que sur les inputs `app_train`).
|
| 8 |
Toutes les valeurs respectent les bornes définies dans `api/schemas.py`.
|
| 9 |
|
| 10 |
---
|
|
|
|
| 17 |
|
| 18 |
```json
|
| 19 |
{
|
| 20 |
+
"SK_ID_CURR": 282751,
|
| 21 |
"NAME_CONTRACT_TYPE": "Cash loans",
|
| 22 |
"CODE_GENDER": "F",
|
| 23 |
"FLAG_OWN_CAR": "Y",
|
|
|
|
| 57 |
"REG_CITY_NOT_WORK_CITY": 0,
|
| 58 |
"LIVE_CITY_NOT_WORK_CITY": 0,
|
| 59 |
"ORGANIZATION_TYPE": "Bank",
|
| 60 |
+
"EXT_SOURCE_1": 0.80,
|
| 61 |
+
"EXT_SOURCE_2": 0.85,
|
| 62 |
+
"EXT_SOURCE_3": 0.78,
|
| 63 |
"APARTMENTS_AVG": 0.1242,
|
| 64 |
"BASEMENTAREA_AVG": 0.0876,
|
| 65 |
"YEARS_BEGINEXPLUATATION_AVG": 0.9821,
|
|
|
|
| 150 |
|
| 151 |
```json
|
| 152 |
{
|
| 153 |
+
"SK_ID_CURR": 207397,
|
| 154 |
"NAME_CONTRACT_TYPE": "Cash loans",
|
| 155 |
"CODE_GENDER": "M",
|
| 156 |
"FLAG_OWN_CAR": "N",
|
|
|
|
| 191 |
"LIVE_CITY_NOT_WORK_CITY": 1,
|
| 192 |
"ORGANIZATION_TYPE": "Industry: type 3",
|
| 193 |
"EXT_SOURCE_1": 0.42,
|
| 194 |
+
"EXT_SOURCE_2": 0.48,
|
| 195 |
+
"EXT_SOURCE_3": 0.39,
|
| 196 |
"APARTMENTS_AVG": null,
|
| 197 |
"BASEMENTAREA_AVG": null,
|
| 198 |
"YEARS_BEGINEXPLUATATION_AVG": null,
|
|
|
|
| 284 |
|
| 285 |
```json
|
| 286 |
{
|
| 287 |
+
"SK_ID_CURR": 244757,
|
| 288 |
"NAME_CONTRACT_TYPE": "Cash loans",
|
| 289 |
"CODE_GENDER": "M",
|
| 290 |
"FLAG_OWN_CAR": "N",
|
|
|
|
| 324 |
"REG_CITY_NOT_WORK_CITY": 1,
|
| 325 |
"LIVE_CITY_NOT_WORK_CITY": 1,
|
| 326 |
"ORGANIZATION_TYPE": "Construction",
|
| 327 |
+
"EXT_SOURCE_1": 0.08,
|
| 328 |
+
"EXT_SOURCE_2": 0.10,
|
| 329 |
+
"EXT_SOURCE_3": 0.06,
|
| 330 |
+
"APARTMENTS_AVG": null,
|
| 331 |
+
"BASEMENTAREA_AVG": null,
|
| 332 |
+
"YEARS_BEGINEXPLUATATION_AVG": null,
|
| 333 |
+
"YEARS_BUILD_AVG": null,
|
| 334 |
+
"COMMONAREA_AVG": null,
|
| 335 |
+
"ELEVATORS_AVG": null,
|
| 336 |
+
"ENTRANCES_AVG": null,
|
| 337 |
+
"FLOORSMAX_AVG": null,
|
| 338 |
+
"FLOORSMIN_AVG": null,
|
| 339 |
+
"LANDAREA_AVG": null,
|
| 340 |
+
"LIVINGAPARTMENTS_AVG": null,
|
| 341 |
+
"LIVINGAREA_AVG": null,
|
| 342 |
+
"NONLIVINGAPARTMENTS_AVG": null,
|
| 343 |
+
"NONLIVINGAREA_AVG": null,
|
| 344 |
+
"APARTMENTS_MODE": null,
|
| 345 |
+
"BASEMENTAREA_MODE": null,
|
| 346 |
+
"YEARS_BEGINEXPLUATATION_MODE": null,
|
| 347 |
+
"YEARS_BUILD_MODE": null,
|
| 348 |
+
"COMMONAREA_MODE": null,
|
| 349 |
+
"ELEVATORS_MODE": null,
|
| 350 |
+
"ENTRANCES_MODE": null,
|
| 351 |
+
"FLOORSMAX_MODE": null,
|
| 352 |
+
"FLOORSMIN_MODE": null,
|
| 353 |
+
"LANDAREA_MODE": null,
|
| 354 |
+
"LIVINGAPARTMENTS_MODE": null,
|
| 355 |
+
"LIVINGAREA_MODE": null,
|
| 356 |
+
"NONLIVINGAPARTMENTS_MODE": null,
|
| 357 |
+
"NONLIVINGAREA_MODE": null,
|
| 358 |
+
"APARTMENTS_MEDI": null,
|
| 359 |
+
"BASEMENTAREA_MEDI": null,
|
| 360 |
+
"YEARS_BEGINEXPLUATATION_MEDI": null,
|
| 361 |
+
"YEARS_BUILD_MEDI": null,
|
| 362 |
+
"COMMONAREA_MEDI": null,
|
| 363 |
+
"ELEVATORS_MEDI": null,
|
| 364 |
+
"ENTRANCES_MEDI": null,
|
| 365 |
+
"FLOORSMAX_MEDI": null,
|
| 366 |
+
"FLOORSMIN_MEDI": null,
|
| 367 |
+
"LANDAREA_MEDI": null,
|
| 368 |
+
"LIVINGAPARTMENTS_MEDI": null,
|
| 369 |
+
"LIVINGAREA_MEDI": null,
|
| 370 |
+
"NONLIVINGAPARTMENTS_MEDI": null,
|
| 371 |
+
"NONLIVINGAREA_MEDI": null,
|
| 372 |
+
"FONDKAPREMONT_MODE": null,
|
| 373 |
+
"HOUSETYPE_MODE": null,
|
| 374 |
+
"TOTALAREA_MODE": null,
|
| 375 |
+
"WALLSMATERIAL_MODE": null,
|
| 376 |
+
"EMERGENCYSTATE_MODE": null,
|
| 377 |
+
"OBS_30_CNT_SOCIAL_CIRCLE": 7,
|
| 378 |
+
"DEF_30_CNT_SOCIAL_CIRCLE": 3,
|
| 379 |
+
"OBS_60_CNT_SOCIAL_CIRCLE": 8,
|
| 380 |
+
"DEF_60_CNT_SOCIAL_CIRCLE": 4,
|
| 381 |
+
"DAYS_LAST_PHONE_CHANGE": -45,
|
| 382 |
+
"FLAG_DOCUMENT_2": 0,
|
| 383 |
+
"FLAG_DOCUMENT_3": 1,
|
| 384 |
+
"FLAG_DOCUMENT_4": 0,
|
| 385 |
+
"FLAG_DOCUMENT_5": 0,
|
| 386 |
+
"FLAG_DOCUMENT_6": 0,
|
| 387 |
+
"FLAG_DOCUMENT_7": 0,
|
| 388 |
+
"FLAG_DOCUMENT_8": 0,
|
| 389 |
+
"FLAG_DOCUMENT_9": 0,
|
| 390 |
+
"FLAG_DOCUMENT_10": 0,
|
| 391 |
+
"FLAG_DOCUMENT_11": 0,
|
| 392 |
+
"FLAG_DOCUMENT_12": 0,
|
| 393 |
+
"FLAG_DOCUMENT_13": 0,
|
| 394 |
+
"FLAG_DOCUMENT_14": 0,
|
| 395 |
+
"FLAG_DOCUMENT_15": 0,
|
| 396 |
+
"FLAG_DOCUMENT_16": 0,
|
| 397 |
+
"FLAG_DOCUMENT_17": 0,
|
| 398 |
+
"FLAG_DOCUMENT_18": 0,
|
| 399 |
+
"FLAG_DOCUMENT_19": 0,
|
| 400 |
+
"FLAG_DOCUMENT_20": 0,
|
| 401 |
+
"FLAG_DOCUMENT_21": 0,
|
| 402 |
+
"AMT_REQ_CREDIT_BUREAU_HOUR": 0,
|
| 403 |
+
"AMT_REQ_CREDIT_BUREAU_DAY": 1,
|
| 404 |
+
"AMT_REQ_CREDIT_BUREAU_WEEK": 2,
|
| 405 |
+
"AMT_REQ_CREDIT_BUREAU_MON": 4,
|
| 406 |
+
"AMT_REQ_CREDIT_BUREAU_QRT": 6,
|
| 407 |
+
"AMT_REQ_CREDIT_BUREAU_YEAR": 14
|
| 408 |
+
}
|
| 409 |
+
```
|
| 410 |
+
|
| 411 |
+
---
|
| 412 |
+
|
| 413 |
+
## 4) Client **inconnu** — bon profil
|
| 414 |
+
|
| 415 |
+
`SK_ID_CURR=999001` n'existe pas dans `features_store.parquet` → l'API
|
| 416 |
+
remplit toutes les agrégations bureau / prev / POS / CC / install avec le
|
| 417 |
+
`no_history_template` (counts=0, autres NaN). Le score repose alors
|
| 418 |
+
uniquement sur les inputs `app_train`. Mêmes valeurs que le profil 1
|
| 419 |
+
(cadre stable, EXT_SOURCE élevés) mais SK_ID hors du référentiel.
|
| 420 |
+
|
| 421 |
+
```json
|
| 422 |
+
{
|
| 423 |
+
"SK_ID_CURR": 999001,
|
| 424 |
+
"NAME_CONTRACT_TYPE": "Cash loans",
|
| 425 |
+
"CODE_GENDER": "F",
|
| 426 |
+
"FLAG_OWN_CAR": "Y",
|
| 427 |
+
"FLAG_OWN_REALTY": "Y",
|
| 428 |
+
"CNT_CHILDREN": 1,
|
| 429 |
+
"AMT_INCOME_TOTAL": 270000,
|
| 430 |
+
"AMT_CREDIT": 600000,
|
| 431 |
+
"AMT_ANNUITY": 28500,
|
| 432 |
+
"AMT_GOODS_PRICE": 540000,
|
| 433 |
+
"NAME_TYPE_SUITE": "Unaccompanied",
|
| 434 |
+
"NAME_INCOME_TYPE": "Commercial associate",
|
| 435 |
+
"NAME_EDUCATION_TYPE": "Higher education",
|
| 436 |
+
"NAME_FAMILY_STATUS": "Married",
|
| 437 |
+
"NAME_HOUSING_TYPE": "House / apartment",
|
| 438 |
+
"REGION_POPULATION_RELATIVE": 0.035792,
|
| 439 |
+
"DAYS_BIRTH": -15400,
|
| 440 |
+
"DAYS_EMPLOYED": -3650,
|
| 441 |
+
"DAYS_REGISTRATION": -4200.0,
|
| 442 |
+
"DAYS_ID_PUBLISH": -2800,
|
| 443 |
+
"OWN_CAR_AGE": 4,
|
| 444 |
+
"FLAG_MOBIL": 1,
|
| 445 |
+
"FLAG_EMP_PHONE": 1,
|
| 446 |
+
"FLAG_WORK_PHONE": 0,
|
| 447 |
+
"FLAG_CONT_MOBILE": 1,
|
| 448 |
+
"FLAG_PHONE": 1,
|
| 449 |
+
"FLAG_EMAIL": 1,
|
| 450 |
+
"OCCUPATION_TYPE": "Managers",
|
| 451 |
+
"CNT_FAM_MEMBERS": 3,
|
| 452 |
+
"REGION_RATING_CLIENT": 2,
|
| 453 |
+
"REGION_RATING_CLIENT_W_CITY": 2,
|
| 454 |
+
"WEEKDAY_APPR_PROCESS_START": "WEDNESDAY",
|
| 455 |
+
"HOUR_APPR_PROCESS_START": 11,
|
| 456 |
+
"REG_REGION_NOT_LIVE_REGION": 0,
|
| 457 |
+
"REG_REGION_NOT_WORK_REGION": 0,
|
| 458 |
+
"LIVE_REGION_NOT_WORK_REGION": 0,
|
| 459 |
+
"REG_CITY_NOT_LIVE_CITY": 0,
|
| 460 |
+
"REG_CITY_NOT_WORK_CITY": 0,
|
| 461 |
+
"LIVE_CITY_NOT_WORK_CITY": 0,
|
| 462 |
+
"ORGANIZATION_TYPE": "Bank",
|
| 463 |
+
"EXT_SOURCE_1": 0.80,
|
| 464 |
+
"EXT_SOURCE_2": 0.85,
|
| 465 |
+
"EXT_SOURCE_3": 0.78,
|
| 466 |
+
"APARTMENTS_AVG": 0.1242,
|
| 467 |
+
"BASEMENTAREA_AVG": 0.0876,
|
| 468 |
+
"YEARS_BEGINEXPLUATATION_AVG": 0.9821,
|
| 469 |
+
"YEARS_BUILD_AVG": 0.7553,
|
| 470 |
+
"COMMONAREA_AVG": 0.0432,
|
| 471 |
+
"ELEVATORS_AVG": 0.08,
|
| 472 |
+
"ENTRANCES_AVG": 0.1379,
|
| 473 |
+
"FLOORSMAX_AVG": 0.1667,
|
| 474 |
+
"FLOORSMIN_AVG": 0.2083,
|
| 475 |
+
"LANDAREA_AVG": 0.0481,
|
| 476 |
+
"LIVINGAPARTMENTS_AVG": 0.0973,
|
| 477 |
+
"LIVINGAREA_AVG": 0.1099,
|
| 478 |
+
"NONLIVINGAPARTMENTS_AVG": 0.0039,
|
| 479 |
+
"NONLIVINGAREA_AVG": 0.0144,
|
| 480 |
+
"APARTMENTS_MODE": 0.1257,
|
| 481 |
+
"BASEMENTAREA_MODE": 0.0901,
|
| 482 |
+
"YEARS_BEGINEXPLUATATION_MODE": 0.9826,
|
| 483 |
+
"YEARS_BUILD_MODE": 0.7649,
|
| 484 |
+
"COMMONAREA_MODE": 0.0413,
|
| 485 |
+
"ELEVATORS_MODE": 0.0775,
|
| 486 |
+
"ENTRANCES_MODE": 0.1379,
|
| 487 |
+
"FLOORSMAX_MODE": 0.1667,
|
| 488 |
+
"FLOORSMIN_MODE": 0.2083,
|
| 489 |
+
"LANDAREA_MODE": 0.0492,
|
| 490 |
+
"LIVINGAPARTMENTS_MODE": 0.0991,
|
| 491 |
+
"LIVINGAREA_MODE": 0.1129,
|
| 492 |
+
"NONLIVINGAPARTMENTS_MODE": 0.0036,
|
| 493 |
+
"NONLIVINGAREA_MODE": 0.0151,
|
| 494 |
+
"APARTMENTS_MEDI": 0.1252,
|
| 495 |
+
"BASEMENTAREA_MEDI": 0.0884,
|
| 496 |
+
"YEARS_BEGINEXPLUATATION_MEDI": 0.9822,
|
| 497 |
+
"YEARS_BUILD_MEDI": 0.7585,
|
| 498 |
+
"COMMONAREA_MEDI": 0.0432,
|
| 499 |
+
"ELEVATORS_MEDI": 0.08,
|
| 500 |
+
"ENTRANCES_MEDI": 0.1379,
|
| 501 |
+
"FLOORSMAX_MEDI": 0.1667,
|
| 502 |
+
"FLOORSMIN_MEDI": 0.2083,
|
| 503 |
+
"LANDAREA_MEDI": 0.0482,
|
| 504 |
+
"LIVINGAPARTMENTS_MEDI": 0.0978,
|
| 505 |
+
"LIVINGAREA_MEDI": 0.1109,
|
| 506 |
+
"NONLIVINGAPARTMENTS_MEDI": 0.0039,
|
| 507 |
+
"NONLIVINGAREA_MEDI": 0.0148,
|
| 508 |
+
"FONDKAPREMONT_MODE": "reg oper account",
|
| 509 |
+
"HOUSETYPE_MODE": "block of flats",
|
| 510 |
+
"TOTALAREA_MODE": 0.1023,
|
| 511 |
+
"WALLSMATERIAL_MODE": "Panel",
|
| 512 |
+
"EMERGENCYSTATE_MODE": "No",
|
| 513 |
+
"OBS_30_CNT_SOCIAL_CIRCLE": 1,
|
| 514 |
+
"DEF_30_CNT_SOCIAL_CIRCLE": 0,
|
| 515 |
+
"OBS_60_CNT_SOCIAL_CIRCLE": 1,
|
| 516 |
+
"DEF_60_CNT_SOCIAL_CIRCLE": 0,
|
| 517 |
+
"DAYS_LAST_PHONE_CHANGE": -1850,
|
| 518 |
+
"FLAG_DOCUMENT_2": 0,
|
| 519 |
+
"FLAG_DOCUMENT_3": 1,
|
| 520 |
+
"FLAG_DOCUMENT_4": 0,
|
| 521 |
+
"FLAG_DOCUMENT_5": 0,
|
| 522 |
+
"FLAG_DOCUMENT_6": 0,
|
| 523 |
+
"FLAG_DOCUMENT_7": 0,
|
| 524 |
+
"FLAG_DOCUMENT_8": 0,
|
| 525 |
+
"FLAG_DOCUMENT_9": 0,
|
| 526 |
+
"FLAG_DOCUMENT_10": 0,
|
| 527 |
+
"FLAG_DOCUMENT_11": 0,
|
| 528 |
+
"FLAG_DOCUMENT_12": 0,
|
| 529 |
+
"FLAG_DOCUMENT_13": 0,
|
| 530 |
+
"FLAG_DOCUMENT_14": 0,
|
| 531 |
+
"FLAG_DOCUMENT_15": 0,
|
| 532 |
+
"FLAG_DOCUMENT_16": 0,
|
| 533 |
+
"FLAG_DOCUMENT_17": 0,
|
| 534 |
+
"FLAG_DOCUMENT_18": 0,
|
| 535 |
+
"FLAG_DOCUMENT_19": 0,
|
| 536 |
+
"FLAG_DOCUMENT_20": 0,
|
| 537 |
+
"FLAG_DOCUMENT_21": 0,
|
| 538 |
+
"AMT_REQ_CREDIT_BUREAU_HOUR": 0,
|
| 539 |
+
"AMT_REQ_CREDIT_BUREAU_DAY": 0,
|
| 540 |
+
"AMT_REQ_CREDIT_BUREAU_WEEK": 0,
|
| 541 |
+
"AMT_REQ_CREDIT_BUREAU_MON": 0,
|
| 542 |
+
"AMT_REQ_CREDIT_BUREAU_QRT": 0,
|
| 543 |
+
"AMT_REQ_CREDIT_BUREAU_YEAR": 2
|
| 544 |
+
}
|
| 545 |
+
```
|
| 546 |
+
|
| 547 |
+
---
|
| 548 |
+
|
| 549 |
+
## 5) Client **inconnu** — mauvais profil
|
| 550 |
+
|
| 551 |
+
`SK_ID_CURR=999002` absent du référentiel → `no_history_template` appliqué.
|
| 552 |
+
Mêmes signaux app_train que le profil 3 (jeune, faibles revenus, EXT_SOURCE
|
| 553 |
+
très bas, sollicitations bureau nombreuses, défauts sociaux).
|
| 554 |
+
|
| 555 |
+
```json
|
| 556 |
+
{
|
| 557 |
+
"SK_ID_CURR": 999002,
|
| 558 |
+
"NAME_CONTRACT_TYPE": "Cash loans",
|
| 559 |
+
"CODE_GENDER": "M",
|
| 560 |
+
"FLAG_OWN_CAR": "N",
|
| 561 |
+
"FLAG_OWN_REALTY": "N",
|
| 562 |
+
"CNT_CHILDREN": 2,
|
| 563 |
+
"AMT_INCOME_TOTAL": 81000,
|
| 564 |
+
"AMT_CREDIT": 720000,
|
| 565 |
+
"AMT_ANNUITY": 42300,
|
| 566 |
+
"AMT_GOODS_PRICE": 675000,
|
| 567 |
+
"NAME_TYPE_SUITE": "Family",
|
| 568 |
+
"NAME_INCOME_TYPE": "Working",
|
| 569 |
+
"NAME_EDUCATION_TYPE": "Secondary / secondary special",
|
| 570 |
+
"NAME_FAMILY_STATUS": "Civil marriage",
|
| 571 |
+
"NAME_HOUSING_TYPE": "With parents",
|
| 572 |
+
"REGION_POPULATION_RELATIVE": 0.008575,
|
| 573 |
+
"DAYS_BIRTH": -9125,
|
| 574 |
+
"DAYS_EMPLOYED": -240,
|
| 575 |
+
"DAYS_REGISTRATION": -800.0,
|
| 576 |
+
"DAYS_ID_PUBLISH": -450,
|
| 577 |
+
"OWN_CAR_AGE": null,
|
| 578 |
+
"FLAG_MOBIL": 1,
|
| 579 |
+
"FLAG_EMP_PHONE": 1,
|
| 580 |
+
"FLAG_WORK_PHONE": 0,
|
| 581 |
+
"FLAG_CONT_MOBILE": 1,
|
| 582 |
+
"FLAG_PHONE": 0,
|
| 583 |
+
"FLAG_EMAIL": 0,
|
| 584 |
+
"OCCUPATION_TYPE": "Low-skill Laborers",
|
| 585 |
+
"CNT_FAM_MEMBERS": 4,
|
| 586 |
+
"REGION_RATING_CLIENT": 3,
|
| 587 |
+
"REGION_RATING_CLIENT_W_CITY": 3,
|
| 588 |
+
"WEEKDAY_APPR_PROCESS_START": "SATURDAY",
|
| 589 |
+
"HOUR_APPR_PROCESS_START": 19,
|
| 590 |
+
"REG_REGION_NOT_LIVE_REGION": 1,
|
| 591 |
+
"REG_REGION_NOT_WORK_REGION": 1,
|
| 592 |
+
"LIVE_REGION_NOT_WORK_REGION": 0,
|
| 593 |
+
"REG_CITY_NOT_LIVE_CITY": 1,
|
| 594 |
+
"REG_CITY_NOT_WORK_CITY": 1,
|
| 595 |
+
"LIVE_CITY_NOT_WORK_CITY": 1,
|
| 596 |
+
"ORGANIZATION_TYPE": "Construction",
|
| 597 |
+
"EXT_SOURCE_1": 0.08,
|
| 598 |
+
"EXT_SOURCE_2": 0.10,
|
| 599 |
+
"EXT_SOURCE_3": 0.06,
|
| 600 |
"APARTMENTS_AVG": null,
|
| 601 |
"BASEMENTAREA_AVG": null,
|
| 602 |
"YEARS_BEGINEXPLUATATION_AVG": null,
|
api/predictor.py
CHANGED
|
@@ -12,7 +12,6 @@ import json
|
|
| 12 |
from pathlib import Path
|
| 13 |
|
| 14 |
import joblib
|
| 15 |
-
import numpy as np
|
| 16 |
import pandas as pd
|
| 17 |
|
| 18 |
from api.schemas import Decision
|
|
@@ -22,7 +21,9 @@ class CreditScoringPredictor:
|
|
| 22 |
"""Singleton-style wrapper. Build once via load(), reuse for every request."""
|
| 23 |
|
| 24 |
def __init__(self, model, threshold: float, model_version: str) -> None:
|
| 25 |
-
|
|
|
|
|
|
|
| 26 |
self._threshold = threshold
|
| 27 |
self._model_version = model_version
|
| 28 |
|
|
@@ -33,14 +34,13 @@ class CreditScoringPredictor:
|
|
| 33 |
model_info_path: Path,
|
| 34 |
default_threshold: float,
|
| 35 |
) -> "CreditScoringPredictor":
|
| 36 |
-
model = joblib.load(model_path)
|
| 37 |
-
|
| 38 |
info = json.loads(model_info_path.read_text())
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
| 44 |
|
| 45 |
@property
|
| 46 |
def threshold(self) -> float:
|
|
@@ -52,20 +52,6 @@ class CreditScoringPredictor:
|
|
| 52 |
|
| 53 |
def predict(self, features: pd.DataFrame) -> tuple[float, Decision]:
|
| 54 |
"""Return (probability_of_default, decision)."""
|
| 55 |
-
proba = self.
|
| 56 |
decision: Decision = "REFUSED" if proba >= self._threshold else "GRANTED"
|
| 57 |
return proba, decision
|
| 58 |
-
|
| 59 |
-
def _predict_proba(self, features: pd.DataFrame) -> float:
|
| 60 |
-
"""Extract the positive-class probability from the underlying model."""
|
| 61 |
-
raw = self._model.predict(features)
|
| 62 |
-
|
| 63 |
-
# MLflow PyFunc models return arrays; sklearn classifiers return 2D probs.
|
| 64 |
-
arr = np.asarray(raw)
|
| 65 |
-
if arr.ndim == 2 and arr.shape[1] == 2:
|
| 66 |
-
return float(arr[0, 1])
|
| 67 |
-
if arr.ndim == 1:
|
| 68 |
-
return float(arr[0])
|
| 69 |
-
raise ValueError(
|
| 70 |
-
f"Unexpected prediction shape {arr.shape}; expected (n,) or (n, 2)."
|
| 71 |
-
)
|
|
|
|
| 12 |
from pathlib import Path
|
| 13 |
|
| 14 |
import joblib
|
|
|
|
| 15 |
import pandas as pd
|
| 16 |
|
| 17 |
from api.schemas import Decision
|
|
|
|
| 21 |
"""Singleton-style wrapper. Build once via load(), reuse for every request."""
|
| 22 |
|
| 23 |
def __init__(self, model, threshold: float, model_version: str) -> None:
|
| 24 |
+
# MLflow PyFunc wraps the sklearn model; unwrap so we can call
|
| 25 |
+
# predict_proba (PyFunc.predict() returns class labels, not probas).
|
| 26 |
+
self._model = model.get_raw_model() if hasattr(model, "get_raw_model") else model
|
| 27 |
self._threshold = threshold
|
| 28 |
self._model_version = model_version
|
| 29 |
|
|
|
|
| 34 |
model_info_path: Path,
|
| 35 |
default_threshold: float,
|
| 36 |
) -> "CreditScoringPredictor":
|
|
|
|
|
|
|
| 37 |
info = json.loads(model_info_path.read_text())
|
| 38 |
+
threshold = float(info.get("metrics", {}).get("best_threshold_mean", default_threshold))
|
| 39 |
+
return cls(
|
| 40 |
+
model=joblib.load(model_path),
|
| 41 |
+
threshold=threshold,
|
| 42 |
+
model_version=str(info.get("version", "unknown")),
|
| 43 |
+
)
|
| 44 |
|
| 45 |
@property
|
| 46 |
def threshold(self) -> float:
|
|
|
|
| 52 |
|
| 53 |
def predict(self, features: pd.DataFrame) -> tuple[float, Decision]:
|
| 54 |
"""Return (probability_of_default, decision)."""
|
| 55 |
+
proba = float(self._model.predict_proba(features)[0, 1])
|
| 56 |
decision: Decision = "REFUSED" if proba >= self._threshold else "GRANTED"
|
| 57 |
return proba, decision
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/conftest.py
CHANGED
|
@@ -172,13 +172,13 @@ VALID_PAYLOAD: dict[str, Any] = {
|
|
| 172 |
|
| 173 |
|
| 174 |
class FakeModel:
|
| 175 |
-
"""Minimal stand-in for the
|
| 176 |
|
| 177 |
Returns a probability driven by AMT_INCOME_TOTAL / AMT_CREDIT so tests can
|
| 178 |
deterministically exercise both GRANTED and REFUSED branches.
|
| 179 |
"""
|
| 180 |
|
| 181 |
-
def
|
| 182 |
income = float(df.iloc[0].get("AMT_INCOME_TOTAL", 0) or 0)
|
| 183 |
credit = float(df.iloc[0].get("AMT_CREDIT", 1) or 1)
|
| 184 |
ratio = income / max(credit, 1)
|
|
|
|
| 172 |
|
| 173 |
|
| 174 |
class FakeModel:
|
| 175 |
+
"""Minimal stand-in for the unwrapped LightGBM sklearn classifier.
|
| 176 |
|
| 177 |
Returns a probability driven by AMT_INCOME_TOTAL / AMT_CREDIT so tests can
|
| 178 |
deterministically exercise both GRANTED and REFUSED branches.
|
| 179 |
"""
|
| 180 |
|
| 181 |
+
def predict_proba(self, df: pd.DataFrame) -> np.ndarray:
|
| 182 |
income = float(df.iloc[0].get("AMT_INCOME_TOTAL", 0) or 0)
|
| 183 |
credit = float(df.iloc[0].get("AMT_CREDIT", 1) or 1)
|
| 184 |
ratio = income / max(credit, 1)
|
tests/unit/test_predictor.py
CHANGED
|
@@ -13,104 +13,84 @@ import pytest
|
|
| 13 |
from api.predictor import CreditScoringPredictor
|
| 14 |
|
| 15 |
|
| 16 |
-
class
|
|
|
|
|
|
|
| 17 |
def __init__(self, proba: float) -> None:
|
| 18 |
self.proba = proba
|
| 19 |
|
| 20 |
-
def
|
| 21 |
return np.array([[1 - self.proba, self.proba]])
|
| 22 |
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
class _FlatModel:
|
| 30 |
-
def predict(self, df: pd.DataFrame) -> np.ndarray:
|
| 31 |
-
return np.array([0.6])
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
def _write_model_info(path: Path, threshold: float, version: str = "test-1") -> None:
|
| 35 |
-
path.write_text(
|
| 36 |
-
json.dumps(
|
| 37 |
-
{
|
| 38 |
-
"model_name": "fake",
|
| 39 |
-
"version": version,
|
| 40 |
-
"metrics": {"best_threshold_mean": threshold},
|
| 41 |
-
}
|
| 42 |
-
)
|
| 43 |
-
)
|
| 44 |
|
| 45 |
|
| 46 |
def _build(tmp_path: Path, proba: float, threshold: float) -> CreditScoringPredictor:
|
| 47 |
model_path = tmp_path / "model.joblib"
|
| 48 |
-
|
| 49 |
-
joblib.dump(_FixedProbModel(proba), model_path)
|
| 50 |
-
_write_model_info(info_path, threshold)
|
| 51 |
return CreditScoringPredictor.load(
|
| 52 |
-
model_path=model_path,
|
|
|
|
|
|
|
| 53 |
)
|
| 54 |
|
| 55 |
|
| 56 |
def test_threshold_loaded_from_model_info(tmp_path):
|
| 57 |
-
|
| 58 |
-
assert pred.threshold == 0.42
|
| 59 |
|
| 60 |
|
| 61 |
def test_threshold_falls_back_to_default(tmp_path):
|
| 62 |
"""If model_info lacks best_threshold_mean, default applies."""
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
| 68 |
assert pred.threshold == 0.7
|
| 69 |
|
| 70 |
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
-
def test_decision_refused_above_threshold(tmp_path):
|
| 79 |
-
pred = _build(tmp_path, proba=0.5, threshold=0.33)
|
| 80 |
-
proba, decision = pred.predict(pd.DataFrame([{}]))
|
| 81 |
-
assert decision == "REFUSED"
|
| 82 |
|
|
|
|
|
|
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
_, decision = pred.predict(pd.DataFrame([{}]))
|
| 88 |
-
assert decision == "REFUSED"
|
| 89 |
|
|
|
|
|
|
|
| 90 |
|
| 91 |
-
def
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
proba, _ = pred.predict(pd.DataFrame([{}]))
|
| 94 |
-
assert
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
def test_unexpected_prediction_shape_raises(tmp_path):
|
| 98 |
-
info = tmp_path / "info.json"
|
| 99 |
-
_write_model_info(info, 0.5)
|
| 100 |
-
model = tmp_path / "model.joblib"
|
| 101 |
-
joblib.dump(_WeirdModel(), model)
|
| 102 |
-
pred = CreditScoringPredictor.load(model, info, default_threshold=0.5)
|
| 103 |
-
with pytest.raises(ValueError, match="Unexpected prediction shape"):
|
| 104 |
-
pred.predict(pd.DataFrame([{}]))
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
def test_1d_array_prediction_supported(tmp_path):
|
| 108 |
-
"""Some PyFunc wrappers return a 1D probability array — supported too."""
|
| 109 |
-
info = tmp_path / "info.json"
|
| 110 |
-
_write_model_info(info, 0.5)
|
| 111 |
-
model = tmp_path / "model.joblib"
|
| 112 |
-
joblib.dump(_FlatModel(), model)
|
| 113 |
-
pred = CreditScoringPredictor.load(model, info, default_threshold=0.5)
|
| 114 |
-
proba, decision = pred.predict(pd.DataFrame([{}]))
|
| 115 |
-
assert proba == pytest.approx(0.6)
|
| 116 |
-
assert decision == "REFUSED"
|
|
|
|
| 13 |
from api.predictor import CreditScoringPredictor
|
| 14 |
|
| 15 |
|
| 16 |
+
class _FakeClassifier:
|
| 17 |
+
"""Stand-in sklearn-style classifier: returns a fixed positive-class proba."""
|
| 18 |
+
|
| 19 |
def __init__(self, proba: float) -> None:
|
| 20 |
self.proba = proba
|
| 21 |
|
| 22 |
+
def predict_proba(self, df: pd.DataFrame) -> np.ndarray:
|
| 23 |
return np.array([[1 - self.proba, self.proba]])
|
| 24 |
|
| 25 |
|
| 26 |
+
def _make_info(path: Path, threshold: float | None = 0.33) -> Path:
|
| 27 |
+
metrics = {"best_threshold_mean": threshold} if threshold is not None else {}
|
| 28 |
+
path.write_text(json.dumps({"version": "test-1", "metrics": metrics}))
|
| 29 |
+
return path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
|
| 32 |
def _build(tmp_path: Path, proba: float, threshold: float) -> CreditScoringPredictor:
|
| 33 |
model_path = tmp_path / "model.joblib"
|
| 34 |
+
joblib.dump(_FakeClassifier(proba), model_path)
|
|
|
|
|
|
|
| 35 |
return CreditScoringPredictor.load(
|
| 36 |
+
model_path=model_path,
|
| 37 |
+
model_info_path=_make_info(tmp_path / "info.json", threshold),
|
| 38 |
+
default_threshold=0.5,
|
| 39 |
)
|
| 40 |
|
| 41 |
|
| 42 |
def test_threshold_loaded_from_model_info(tmp_path):
|
| 43 |
+
assert _build(tmp_path, proba=0.1, threshold=0.42).threshold == 0.42
|
|
|
|
| 44 |
|
| 45 |
|
| 46 |
def test_threshold_falls_back_to_default(tmp_path):
|
| 47 |
"""If model_info lacks best_threshold_mean, default applies."""
|
| 48 |
+
joblib.dump(_FakeClassifier(0.0), tmp_path / "model.joblib")
|
| 49 |
+
pred = CreditScoringPredictor.load(
|
| 50 |
+
model_path=tmp_path / "model.joblib",
|
| 51 |
+
model_info_path=_make_info(tmp_path / "info.json", threshold=None),
|
| 52 |
+
default_threshold=0.7,
|
| 53 |
+
)
|
| 54 |
assert pred.threshold == 0.7
|
| 55 |
|
| 56 |
|
| 57 |
+
@pytest.mark.parametrize(
|
| 58 |
+
"proba, expected",
|
| 59 |
+
[
|
| 60 |
+
(0.10, "GRANTED"), # below threshold
|
| 61 |
+
(0.50, "REFUSED"), # above threshold
|
| 62 |
+
(0.33, "REFUSED"), # boundary: proba >= threshold → REFUSED
|
| 63 |
+
],
|
| 64 |
+
)
|
| 65 |
+
def test_decision_logic(tmp_path, proba, expected):
|
| 66 |
+
p, decision = _build(tmp_path, proba=proba, threshold=0.33).predict(pd.DataFrame([{}]))
|
| 67 |
+
assert p == pytest.approx(proba)
|
| 68 |
+
assert decision == expected
|
| 69 |
+
|
| 70 |
|
| 71 |
+
def test_proba_is_continuous_not_label(tmp_path):
|
| 72 |
+
"""Regression: predict() must surface predict_proba output, not class labels."""
|
| 73 |
+
proba, _ = _build(tmp_path, proba=0.27, threshold=0.5).predict(pd.DataFrame([{}]))
|
| 74 |
+
assert proba == pytest.approx(0.27)
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
+
def test_pyfunc_wrapper_is_unwrapped():
|
| 78 |
+
"""A model exposing get_raw_model() (MLflow PyFunc) is unwrapped at init."""
|
| 79 |
|
| 80 |
+
class _PyFuncLike:
|
| 81 |
+
def __init__(self, inner):
|
| 82 |
+
self._inner = inner
|
|
|
|
|
|
|
| 83 |
|
| 84 |
+
def get_raw_model(self):
|
| 85 |
+
return self._inner
|
| 86 |
|
| 87 |
+
def predict(self, df): # PyFunc would return labels — must NOT be called
|
| 88 |
+
raise AssertionError("PyFunc.predict() must not be called")
|
| 89 |
+
|
| 90 |
+
pred = CreditScoringPredictor(
|
| 91 |
+
model=_PyFuncLike(_FakeClassifier(0.42)),
|
| 92 |
+
threshold=0.5,
|
| 93 |
+
model_version="v1",
|
| 94 |
+
)
|
| 95 |
proba, _ = pred.predict(pd.DataFrame([{}]))
|
| 96 |
+
assert proba == pytest.approx(0.42)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|