2026-01-22 11:45:39 +02:00
|
|
|
|
import time
|
|
|
|
|
|
import joblib
|
2026-01-21 21:08:24 +02:00
|
|
|
|
import pandas as pd
|
|
|
|
|
|
import numpy as np
|
2026-01-22 11:45:39 +02:00
|
|
|
|
import random
|
|
|
|
|
|
|
|
|
|
|
|
from sklearn.base import BaseEstimator, TransformerMixin
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ===== КРИТИЧЕСКИ ВАЖНО =====
|
|
|
|
|
|
# Эти классы ДОЛЖНЫ существовать до joblib.load()
|
|
|
|
|
|
|
|
|
|
|
|
class TextExtractor(BaseEstimator, TransformerMixin):
|
|
|
|
|
|
def fit(self, X, y=None):
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
def transform(self, X):
|
|
|
|
|
|
return X['full_text'].fillna('')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class NumberExtractor(BaseEstimator, TransformerMixin):
|
|
|
|
|
|
def fit(self, X, y=None):
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
def transform(self, X):
|
|
|
|
|
|
return X[['amount']].fillna(0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================
|
|
|
|
|
|
|
|
|
|
|
|
MODEL_PATH = "mcc_model.pkl"
|
|
|
|
|
|
|
|
|
|
|
|
BASE_TX = {
|
|
|
|
|
|
"transaction_id": "TX00001116",
|
|
|
|
|
|
"terminal_name": "STORE001",
|
|
|
|
|
|
"terminal_description": "common common common thing",
|
|
|
|
|
|
"city": "NYC",
|
|
|
|
|
|
"amount": 272.80,
|
|
|
|
|
|
"items": [
|
|
|
|
|
|
{"name": "basic loyalty", "price": 58.20},
|
|
|
|
|
|
{"name": "Bringiong item lifes", "price": 28.99},
|
|
|
|
|
|
{"name": "regular item basic item", "price": 56.91}
|
|
|
|
|
|
]
|
2026-01-21 21:08:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 11:45:39 +02:00
|
|
|
|
|
|
|
|
|
|
def json_to_df(data_list):
|
|
|
|
|
|
rows = []
|
|
|
|
|
|
for d in data_list:
|
|
|
|
|
|
items = d.get("items", [])
|
|
|
|
|
|
item_text = " ".join(str(i.get("name","")) for i in items)
|
|
|
|
|
|
full_text = f"{d.get('terminal_name','')} {d.get('terminal_description','')} {item_text}".lower()
|
|
|
|
|
|
|
|
|
|
|
|
rows.append({
|
|
|
|
|
|
"full_text": full_text,
|
|
|
|
|
|
"amount": float(d.get("amount", 0))
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return pd.DataFrame(rows)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def make_batch(n=100):
|
|
|
|
|
|
batch = []
|
|
|
|
|
|
for i in range(n):
|
|
|
|
|
|
tx = BASE_TX.copy()
|
|
|
|
|
|
tx["transaction_id"] = f"TX{i:06d}"
|
|
|
|
|
|
tx["amount"] = round(random.uniform(3, 500), 2)
|
|
|
|
|
|
batch.append(tx)
|
|
|
|
|
|
return batch
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================
|
|
|
|
|
|
# BENCH
|
|
|
|
|
|
# ============================
|
|
|
|
|
|
|
|
|
|
|
|
print("\n[1] LOADING MODEL (cold start)...")
|
|
|
|
|
|
t0 = time.perf_counter()
|
|
|
|
|
|
model = joblib.load(MODEL_PATH)
|
|
|
|
|
|
t1 = time.perf_counter()
|
|
|
|
|
|
print(f"Cold load time: {t1 - t0:.4f} sec")
|
|
|
|
|
|
|
|
|
|
|
|
# warmup
|
|
|
|
|
|
print("\n[2] WARMUP...")
|
|
|
|
|
|
warm_df = json_to_df([BASE_TX])
|
|
|
|
|
|
for _ in range(5):
|
|
|
|
|
|
model.predict(warm_df)
|
|
|
|
|
|
|
|
|
|
|
|
# single inference benchmark
|
|
|
|
|
|
print("\n[3] SINGLE REQUEST BENCH (1000 runs)...")
|
|
|
|
|
|
times = []
|
|
|
|
|
|
|
|
|
|
|
|
for _ in range(1000):
|
|
|
|
|
|
df = json_to_df([BASE_TX])
|
|
|
|
|
|
t0 = time.perf_counter()
|
|
|
|
|
|
model.predict_proba(df)
|
|
|
|
|
|
times.append(time.perf_counter() - t0)
|
|
|
|
|
|
|
|
|
|
|
|
times = np.array(times)
|
|
|
|
|
|
|
|
|
|
|
|
print(f"avg : {times.mean()*1000:.2f} ms")
|
|
|
|
|
|
print(f"p95 : {np.percentile(times,95)*1000:.2f} ms")
|
|
|
|
|
|
print(f"max : {times.max()*1000:.2f} ms")
|
|
|
|
|
|
|
|
|
|
|
|
# batch benchmark
|
|
|
|
|
|
print("\n[4] BATCH 100 BENCH...")
|
|
|
|
|
|
batch = make_batch(100)
|
|
|
|
|
|
df_batch = json_to_df(batch)
|
|
|
|
|
|
|
|
|
|
|
|
t0 = time.perf_counter()
|
|
|
|
|
|
model.predict_proba(df_batch)
|
|
|
|
|
|
dt = time.perf_counter() - t0
|
|
|
|
|
|
|
|
|
|
|
|
print(f"Batch 100 time: {dt:.3f} sec")
|
|
|
|
|
|
|
|
|
|
|
|
# verdict
|
|
|
|
|
|
print("\n[5] VERDICT")
|
|
|
|
|
|
if np.percentile(times,95) < 0.2:
|
|
|
|
|
|
print("✅ /predict проходит по latency (<200ms p95)")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("❌ /predict НЕ проходит по latency")
|
|
|
|
|
|
|
|
|
|
|
|
if dt < 5:
|
|
|
|
|
|
print("✅ /predict/batch проходит (<5s)")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("❌ /predict/batch НЕ проходит")
|