Commit ·
b1725f1
1
Parent(s): 94337ad
barely working daemon
Browse files- app/api/background_drift.py +7 -12
- app/api/routes.py +21 -48
- app/api/traffic_daemon.py +3 -9
- app/core/model_registry.py +0 -1
- app/db/models.py +0 -1
- app/db/session.py +0 -1
- app/inference/preprocessing.py +0 -1
- app/monitoring/metrics.py +0 -1
- data/production/predictions_log.csv +9 -6
- reports/evidently/drift_report.html +0 -0
- reports/evidently/drift_report.json +10 -29
app/api/background_drift.py
CHANGED
|
@@ -13,16 +13,10 @@ REFERENCE_PATH = "models/v1/reference_data.csv"
|
|
| 13 |
PROD_LOG_PATH = "data/production/predictions_log.csv"
|
| 14 |
DASHBOARD_JSON = "reports/evidently/drift_report.json"
|
| 15 |
|
| 16 |
-
# Retention policy (VERY IMPORTANT for HF Spaces)
|
| 17 |
MAX_ROWS = 5000 # rolling window
|
| 18 |
-
|
| 19 |
os.makedirs(os.path.dirname(DASHBOARD_JSON), exist_ok=True)
|
| 20 |
|
| 21 |
-
|
| 22 |
async def drift_loop(interval_seconds: int = 10):
|
| 23 |
-
"""
|
| 24 |
-
Continuously compute drift from production inference data.
|
| 25 |
-
"""
|
| 26 |
while True:
|
| 27 |
try:
|
| 28 |
if not os.path.exists(PROD_LOG_PATH):
|
|
@@ -30,13 +24,13 @@ async def drift_loop(interval_seconds: int = 10):
|
|
| 30 |
continue
|
| 31 |
|
| 32 |
prod_df = pd.read_csv(PROD_LOG_PATH)
|
| 33 |
-
|
| 34 |
-
#
|
| 35 |
if len(prod_df) > MAX_ROWS:
|
| 36 |
prod_df = prod_df.tail(MAX_ROWS)
|
| 37 |
prod_df.to_csv(PROD_LOG_PATH, index=False)
|
| 38 |
|
| 39 |
-
#
|
| 40 |
missing_features = set(predictor.features) - set(prod_df.columns)
|
| 41 |
if missing_features:
|
| 42 |
print(f"Skipping drift check, missing features: {missing_features}")
|
|
@@ -50,9 +44,11 @@ async def drift_loop(interval_seconds: int = 10):
|
|
| 50 |
|
| 51 |
reference_df = pd.read_csv(REFERENCE_PATH)
|
| 52 |
|
| 53 |
-
# ----
|
| 54 |
_, drift_dict = run_drift_check(
|
| 55 |
-
prod_df[predictor.features],
|
|
|
|
|
|
|
| 56 |
)
|
| 57 |
|
| 58 |
dashboard_payload = {
|
|
@@ -64,7 +60,6 @@ async def drift_loop(interval_seconds: int = 10):
|
|
| 64 |
],
|
| 65 |
}
|
| 66 |
|
| 67 |
-
# Atomic write (prevents frontend race conditions)
|
| 68 |
tmp_path = DASHBOARD_JSON + ".tmp"
|
| 69 |
with open(tmp_path, "w") as f:
|
| 70 |
json.dump(dashboard_payload, f, indent=2)
|
|
|
|
| 13 |
PROD_LOG_PATH = "data/production/predictions_log.csv"
|
| 14 |
DASHBOARD_JSON = "reports/evidently/drift_report.json"
|
| 15 |
|
|
|
|
| 16 |
MAX_ROWS = 5000 # rolling window
|
|
|
|
| 17 |
os.makedirs(os.path.dirname(DASHBOARD_JSON), exist_ok=True)
|
| 18 |
|
|
|
|
| 19 |
async def drift_loop(interval_seconds: int = 10):
|
|
|
|
|
|
|
|
|
|
| 20 |
while True:
|
| 21 |
try:
|
| 22 |
if not os.path.exists(PROD_LOG_PATH):
|
|
|
|
| 24 |
continue
|
| 25 |
|
| 26 |
prod_df = pd.read_csv(PROD_LOG_PATH)
|
| 27 |
+
|
| 28 |
+
# Retention window
|
| 29 |
if len(prod_df) > MAX_ROWS:
|
| 30 |
prod_df = prod_df.tail(MAX_ROWS)
|
| 31 |
prod_df.to_csv(PROD_LOG_PATH, index=False)
|
| 32 |
|
| 33 |
+
# Keep only rows with all required features
|
| 34 |
missing_features = set(predictor.features) - set(prod_df.columns)
|
| 35 |
if missing_features:
|
| 36 |
print(f"Skipping drift check, missing features: {missing_features}")
|
|
|
|
| 44 |
|
| 45 |
reference_df = pd.read_csv(REFERENCE_PATH)
|
| 46 |
|
| 47 |
+
# ---- Run drift on features only ----
|
| 48 |
_, drift_dict = run_drift_check(
|
| 49 |
+
prod_df[predictor.features],
|
| 50 |
+
reference_df[predictor.features],
|
| 51 |
+
model_version="v1"
|
| 52 |
)
|
| 53 |
|
| 54 |
dashboard_payload = {
|
|
|
|
| 60 |
],
|
| 61 |
}
|
| 62 |
|
|
|
|
| 63 |
tmp_path = DASHBOARD_JSON + ".tmp"
|
| 64 |
with open(tmp_path, "w") as f:
|
| 65 |
json.dump(dashboard_payload, f, indent=2)
|
app/api/routes.py
CHANGED
|
@@ -5,7 +5,6 @@ from fastapi.templating import Jinja2Templates
|
|
| 5 |
|
| 6 |
from app.inference.predictor import Predictor
|
| 7 |
from app.monitoring.data_loader import load_production_data
|
| 8 |
-
from app.monitoring.drift import run_drift_check
|
| 9 |
from app.monitoring.governance import run_governance_checks
|
| 10 |
|
| 11 |
import pandas as pd
|
|
@@ -21,17 +20,17 @@ predictor = Predictor()
|
|
| 21 |
PROD_LOG = "data/production/predictions_log.csv"
|
| 22 |
|
| 23 |
# ------------------------------------------------------------------
|
| 24 |
-
# ENSURE production log exists at server startup
|
| 25 |
# ------------------------------------------------------------------
|
| 26 |
os.makedirs(os.path.dirname(PROD_LOG), exist_ok=True)
|
| 27 |
|
| 28 |
if not os.path.exists(PROD_LOG):
|
| 29 |
-
# Create empty production log with correct schema
|
| 30 |
base_cols = list(predictor.features)
|
| 31 |
extra_cols = [
|
| 32 |
-
"
|
| 33 |
-
"
|
| 34 |
-
"
|
|
|
|
| 35 |
"model_version",
|
| 36 |
"timestamp",
|
| 37 |
]
|
|
@@ -63,35 +62,23 @@ async def predict_file(background_tasks: BackgroundTasks, file: UploadFile = Fil
|
|
| 63 |
"risk_level": "High" if proba >= 0.75 else "Medium" if proba >= 0.5 else "Low"
|
| 64 |
})
|
| 65 |
|
| 66 |
-
# ----
|
| 67 |
-
reference_df = pd.read_csv("models/v1/reference_data.csv")
|
| 68 |
-
_, drift_dict = run_drift_check(
|
| 69 |
-
df[predictor.features],
|
| 70 |
-
reference_df[predictor.features],
|
| 71 |
-
"v1",
|
| 72 |
-
)
|
| 73 |
-
|
| 74 |
-
drift_for_chart = []
|
| 75 |
-
for col, score in drift_dict.items():
|
| 76 |
-
try:
|
| 77 |
-
score_value = float(score)
|
| 78 |
-
if not np.isfinite(score_value):
|
| 79 |
-
score_value = 0.0
|
| 80 |
-
except Exception:
|
| 81 |
-
score_value = 0.0
|
| 82 |
-
drift_for_chart.append({"column": col, "score": score_value})
|
| 83 |
-
|
| 84 |
-
# ---- Append predictions to production log ----
|
| 85 |
df_log = df.copy()
|
| 86 |
|
| 87 |
-
#
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
if col in df_log.columns:
|
| 90 |
df_log = df_log.drop(columns=[col])
|
| 91 |
|
| 92 |
-
df_log["
|
| 93 |
-
df_log["
|
| 94 |
-
df_log["
|
| 95 |
"High" if p >= 0.75 else "Medium" if p >= 0.5 else "Low"
|
| 96 |
for p in probas
|
| 97 |
]
|
|
@@ -100,25 +87,9 @@ async def predict_file(background_tasks: BackgroundTasks, file: UploadFile = Fil
|
|
| 100 |
|
| 101 |
df_log.to_csv(PROD_LOG, mode="a", header=False, index=False)
|
| 102 |
|
| 103 |
-
# ---- Dashboard JSON ----
|
| 104 |
-
DASHBOARD_JSON = "reports/evidently/drift_report.json"
|
| 105 |
-
|
| 106 |
-
dashboard_payload = {
|
| 107 |
-
"n_rows": len(results),
|
| 108 |
-
"results": results,
|
| 109 |
-
"drift": drift_for_chart,
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
os.makedirs(os.path.dirname(DASHBOARD_JSON), exist_ok=True)
|
| 113 |
-
tmp_path = DASHBOARD_JSON + ".tmp"
|
| 114 |
-
with open(tmp_path, "w") as f:
|
| 115 |
-
json.dump(dashboard_payload, f, indent=2)
|
| 116 |
-
os.replace(tmp_path, DASHBOARD_JSON)
|
| 117 |
-
|
| 118 |
return JSONResponse({
|
| 119 |
"n_rows": len(results),
|
| 120 |
"results": results,
|
| 121 |
-
"drift": drift_for_chart,
|
| 122 |
})
|
| 123 |
|
| 124 |
|
|
@@ -130,8 +101,10 @@ def health():
|
|
| 130 |
@router.get("/run-drift")
|
| 131 |
def run_drift():
|
| 132 |
current_df = load_production_data()
|
| 133 |
-
|
| 134 |
-
|
|
|
|
|
|
|
| 135 |
|
| 136 |
|
| 137 |
@router.get("/dashboard")
|
|
|
|
| 5 |
|
| 6 |
from app.inference.predictor import Predictor
|
| 7 |
from app.monitoring.data_loader import load_production_data
|
|
|
|
| 8 |
from app.monitoring.governance import run_governance_checks
|
| 9 |
|
| 10 |
import pandas as pd
|
|
|
|
| 20 |
PROD_LOG = "data/production/predictions_log.csv"
|
| 21 |
|
| 22 |
# ------------------------------------------------------------------
|
| 23 |
+
# ENSURE production log exists at server startup
|
| 24 |
# ------------------------------------------------------------------
|
| 25 |
os.makedirs(os.path.dirname(PROD_LOG), exist_ok=True)
|
| 26 |
|
| 27 |
if not os.path.exists(PROD_LOG):
|
|
|
|
| 28 |
base_cols = list(predictor.features)
|
| 29 |
extra_cols = [
|
| 30 |
+
"target", # true label
|
| 31 |
+
"model_prediction", # model output
|
| 32 |
+
"model_probability",
|
| 33 |
+
"model_risk_level",
|
| 34 |
"model_version",
|
| 35 |
"timestamp",
|
| 36 |
]
|
|
|
|
| 62 |
"risk_level": "High" if proba >= 0.75 else "Medium" if proba >= 0.5 else "Low"
|
| 63 |
})
|
| 64 |
|
| 65 |
+
# ---- Append predictions to production log (minimal, fast) ----
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
df_log = df.copy()
|
| 67 |
|
| 68 |
+
# Keep true target if present
|
| 69 |
+
if "target" in df.columns:
|
| 70 |
+
df_log["target"] = df["target"]
|
| 71 |
+
else:
|
| 72 |
+
df_log["target"] = np.nan
|
| 73 |
+
|
| 74 |
+
# Remove any old model prediction columns to prevent duplicates
|
| 75 |
+
for col in ["model_prediction", "model_probability", "model_risk_level", "model_version", "timestamp"]:
|
| 76 |
if col in df_log.columns:
|
| 77 |
df_log = df_log.drop(columns=[col])
|
| 78 |
|
| 79 |
+
df_log["model_prediction"] = preds
|
| 80 |
+
df_log["model_probability"] = probas
|
| 81 |
+
df_log["model_risk_level"] = [
|
| 82 |
"High" if p >= 0.75 else "Medium" if p >= 0.5 else "Low"
|
| 83 |
for p in probas
|
| 84 |
]
|
|
|
|
| 87 |
|
| 88 |
df_log.to_csv(PROD_LOG, mode="a", header=False, index=False)
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
return JSONResponse({
|
| 91 |
"n_rows": len(results),
|
| 92 |
"results": results,
|
|
|
|
| 93 |
})
|
| 94 |
|
| 95 |
|
|
|
|
| 101 |
@router.get("/run-drift")
|
| 102 |
def run_drift():
|
| 103 |
current_df = load_production_data()
|
| 104 |
+
from app.monitoring.drift import run_drift_check
|
| 105 |
+
reference_df = pd.read_csv("models/v1/reference_data.csv")
|
| 106 |
+
_, drift_dict = run_drift_check(current_df[predictor.features], reference_df[predictor.features])
|
| 107 |
+
return {"status": "drift_check_completed", "drift": drift_dict}
|
| 108 |
|
| 109 |
|
| 110 |
@router.get("/dashboard")
|
app/api/traffic_daemon.py
CHANGED
|
@@ -4,7 +4,6 @@ import pandas as pd
|
|
| 4 |
import random
|
| 5 |
import requests
|
| 6 |
import os
|
| 7 |
-
import time
|
| 8 |
|
| 9 |
API_URL = "http://localhost:8000/predict"
|
| 10 |
SOURCE_DATA = "data/processed/current_data.csv"
|
|
@@ -13,13 +12,9 @@ MIN_SLEEP = 2
|
|
| 13 |
MAX_SLEEP = 8
|
| 14 |
MIN_BATCH = 1
|
| 15 |
MAX_BATCH = 5
|
| 16 |
-
STARTUP_DELAY = 10 #
|
| 17 |
-
|
| 18 |
|
| 19 |
async def traffic_loop():
|
| 20 |
-
"""
|
| 21 |
-
Continuously generate inference traffic against /predict.
|
| 22 |
-
"""
|
| 23 |
await asyncio.sleep(STARTUP_DELAY)
|
| 24 |
|
| 25 |
if not os.path.exists(SOURCE_DATA):
|
|
@@ -27,20 +22,19 @@ async def traffic_loop():
|
|
| 27 |
return
|
| 28 |
|
| 29 |
df = pd.read_csv(SOURCE_DATA)
|
| 30 |
-
|
| 31 |
print("Traffic daemon started.")
|
| 32 |
|
| 33 |
while True:
|
| 34 |
try:
|
| 35 |
batch_size = random.randint(MIN_BATCH, MAX_BATCH)
|
| 36 |
sample = df.sample(batch_size)
|
| 37 |
-
|
| 38 |
csv_bytes = sample.to_csv(index=False).encode("utf-8")
|
| 39 |
|
|
|
|
| 40 |
response = requests.post(
|
| 41 |
API_URL,
|
| 42 |
files={"file": ("sample.csv", csv_bytes, "text/csv")},
|
| 43 |
-
timeout=
|
| 44 |
)
|
| 45 |
|
| 46 |
if response.status_code != 200:
|
|
|
|
| 4 |
import random
|
| 5 |
import requests
|
| 6 |
import os
|
|
|
|
| 7 |
|
| 8 |
API_URL = "http://localhost:8000/predict"
|
| 9 |
SOURCE_DATA = "data/processed/current_data.csv"
|
|
|
|
| 12 |
MAX_SLEEP = 8
|
| 13 |
MIN_BATCH = 1
|
| 14 |
MAX_BATCH = 5
|
| 15 |
+
STARTUP_DELAY = 10 # allow server startup
|
|
|
|
| 16 |
|
| 17 |
async def traffic_loop():
|
|
|
|
|
|
|
|
|
|
| 18 |
await asyncio.sleep(STARTUP_DELAY)
|
| 19 |
|
| 20 |
if not os.path.exists(SOURCE_DATA):
|
|
|
|
| 22 |
return
|
| 23 |
|
| 24 |
df = pd.read_csv(SOURCE_DATA)
|
|
|
|
| 25 |
print("Traffic daemon started.")
|
| 26 |
|
| 27 |
while True:
|
| 28 |
try:
|
| 29 |
batch_size = random.randint(MIN_BATCH, MAX_BATCH)
|
| 30 |
sample = df.sample(batch_size)
|
|
|
|
| 31 |
csv_bytes = sample.to_csv(index=False).encode("utf-8")
|
| 32 |
|
| 33 |
+
# ---- Increased timeout to avoid ReadTimeout ----
|
| 34 |
response = requests.post(
|
| 35 |
API_URL,
|
| 36 |
files={"file": ("sample.csv", csv_bytes, "text/csv")},
|
| 37 |
+
timeout=60,
|
| 38 |
)
|
| 39 |
|
| 40 |
if response.status_code != 200:
|
app/core/model_registry.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
# model loading/versioning
|
|
|
|
|
|
app/db/models.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
# ORM-style tables (optional)
|
|
|
|
|
|
app/db/session.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
# SQLite connection
|
|
|
|
|
|
app/inference/preprocessing.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
# feature handling
|
|
|
|
|
|
app/monitoring/metrics.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
# feature stats extraction
|
|
|
|
|
|
data/production/predictions_log.csv
CHANGED
|
@@ -1,6 +1,9 @@
|
|
| 1 |
-
credit_limit,age,pay_delay_sep,pay_delay_aug,bill_amt_sep,bill_amt_aug,pay_amt_sep,pay_amt_aug,
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
credit_limit,age,pay_delay_sep,pay_delay_aug,bill_amt_sep,bill_amt_aug,pay_amt_sep,pay_amt_aug,target,model_prediction,model_probability,model_risk_level,model_version,timestamp
|
| 2 |
+
70000.0,48,0,0,20744.0,22093.0,2000.0,2000.0,0,0,0.2563046708563498,Low,v1,2026-01-14 19:33:08.201187+00:00
|
| 3 |
+
390000.0,42,0,0,310075.0,184647.0,10021.0,4000.0,0,0,0.07238720996682343,Low,v1,2026-01-14 19:33:08.201187+00:00
|
| 4 |
+
230000.0,50,-2,-2,2789.0,2942.0,2942.0,2520.0,0,0,0.06061841289885345,Low,v1,2026-01-14 19:33:08.201187+00:00
|
| 5 |
+
40000.0,47,2,2,52358.0,54892.0,4000.0,0.0,1,1,0.608097803732095,Medium,v1,2026-01-14 19:35:59.632246+00:00
|
| 6 |
+
140000.0,41,0,0,130138.0,132726.0,4756.0,4912.0,1,0,0.19300589506044843,Low,v1,2026-01-14 19:35:59.632246+00:00
|
| 7 |
+
50000.0,26,-1,-1,5052.0,0.0,0.0,0.0,0,0,0.12125611010883464,Low,v1,2026-01-14 19:37:40.778834+00:00
|
| 8 |
+
400000.0,42,-1,-1,44198.0,10132.0,10132.0,16932.0,0,0,0.06632404342712281,Low,v1,2026-01-14 19:39:17.748870+00:00
|
| 9 |
+
280000.0,45,-2,-2,26573.0,17597.0,18388.0,22302.0,0,0,0.036231471756518835,Low,v1,2026-01-14 19:39:17.748870+00:00
|
reports/evidently/drift_report.html
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
reports/evidently/drift_report.json
CHANGED
|
@@ -1,25 +1,6 @@
|
|
| 1 |
{
|
| 2 |
-
"n_rows":
|
| 3 |
-
"results": [
|
| 4 |
-
{
|
| 5 |
-
"row": 0,
|
| 6 |
-
"probability": 0.0576,
|
| 7 |
-
"prediction": "No Default",
|
| 8 |
-
"risk_level": "Low"
|
| 9 |
-
},
|
| 10 |
-
{
|
| 11 |
-
"row": 1,
|
| 12 |
-
"probability": 0.2814,
|
| 13 |
-
"prediction": "No Default",
|
| 14 |
-
"risk_level": "Low"
|
| 15 |
-
},
|
| 16 |
-
{
|
| 17 |
-
"row": 2,
|
| 18 |
-
"probability": 0.0481,
|
| 19 |
-
"prediction": "No Default",
|
| 20 |
-
"risk_level": "Low"
|
| 21 |
-
}
|
| 22 |
-
],
|
| 23 |
"drift": [
|
| 24 |
{
|
| 25 |
"column": "dataset",
|
|
@@ -27,35 +8,35 @@
|
|
| 27 |
},
|
| 28 |
{
|
| 29 |
"column": "age",
|
| 30 |
-
"score": 0.
|
| 31 |
},
|
| 32 |
{
|
| 33 |
"column": "bill_amt_aug",
|
| 34 |
-
"score": 0.
|
| 35 |
},
|
| 36 |
{
|
| 37 |
"column": "bill_amt_sep",
|
| 38 |
-
"score": 0.
|
| 39 |
},
|
| 40 |
{
|
| 41 |
"column": "credit_limit",
|
| 42 |
-
"score": 0.
|
| 43 |
},
|
| 44 |
{
|
| 45 |
"column": "pay_amt_aug",
|
| 46 |
-
"score":
|
| 47 |
},
|
| 48 |
{
|
| 49 |
"column": "pay_amt_sep",
|
| 50 |
-
"score": 0.
|
| 51 |
},
|
| 52 |
{
|
| 53 |
"column": "pay_delay_aug",
|
| 54 |
-
"score":
|
| 55 |
},
|
| 56 |
{
|
| 57 |
"column": "pay_delay_sep",
|
| 58 |
-
"score": 0.
|
| 59 |
}
|
| 60 |
]
|
| 61 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"n_rows": 8,
|
| 3 |
+
"results": [],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"drift": [
|
| 5 |
{
|
| 6 |
"column": "dataset",
|
|
|
|
| 8 |
},
|
| 9 |
{
|
| 10 |
"column": "age",
|
| 11 |
+
"score": 0.8679104867707123
|
| 12 |
},
|
| 13 |
{
|
| 14 |
"column": "bill_amt_aug",
|
| 15 |
+
"score": 0.23085843753803348
|
| 16 |
},
|
| 17 |
{
|
| 18 |
"column": "bill_amt_sep",
|
| 19 |
+
"score": 0.37457217443848057
|
| 20 |
},
|
| 21 |
{
|
| 22 |
"column": "credit_limit",
|
| 23 |
+
"score": 0.36873847560130574
|
| 24 |
},
|
| 25 |
{
|
| 26 |
"column": "pay_amt_aug",
|
| 27 |
+
"score": 0.20876449446182804
|
| 28 |
},
|
| 29 |
{
|
| 30 |
"column": "pay_amt_sep",
|
| 31 |
+
"score": 0.27529913358402724
|
| 32 |
},
|
| 33 |
{
|
| 34 |
"column": "pay_delay_aug",
|
| 35 |
+
"score": 0.32956762578626103
|
| 36 |
},
|
| 37 |
{
|
| 38 |
"column": "pay_delay_sep",
|
| 39 |
+
"score": 0.4885655407858251
|
| 40 |
}
|
| 41 |
]
|
| 42 |
}
|