13장. REST API 와 FastAPI
실습 3 — 입력 검사와 묶음 예측
실습 2의 API 에 규칙에 어긋난 요청을 보내 422 응답의 생김새를 보고, 시험용 주문 200개를 묶음으로 보내 모델을 바로 부른 결과와 같은지 확인합니다. 실습 1, 2를 먼저 실행해야 합니다.
1단계. 실습 2와 같은 준비
import sys
import joblib
import pandas as pd
sys.path.insert(0, ".")
from fastapi.testclient import TestClient
from out_delay_api import app
client = TestClient(app)
good = {"주문시각": 22, "거리km": 197.6, "무게kg": 4.6, "지역": "지방", "상품분류": "도서", "날씨": "눈", "택배사": "빠른별택배"}
print(client.post("/predict", json=good).status_code)2002단계. 규칙에 어긋난 요청 보내기
bad_cases = {
"칸이 빠짐": {k: v for k, v in good.items() if k != "지역"},
"타입이 틀림": dict(good, 주문시각="밤 10시"),
"범위를 넘음": dict(good, 주문시각=25),
"없는 지역": dict(good, 지역="부산"),
"음수 무게": dict(good, 무게kg=-1),
}
for name, body in bad_cases.items():
r = client.post("/predict", json=body)
err = r.json()["detail"][0] # 어긋난 칸의 위치(loc)와 까닭(msg)
print(f"{name}: {r.status_code} · {err['loc'][-1]} · {err['msg']}")
# 날씨는 기본값이 있어 빠져도 된다
print("날씨 빠짐:", client.post("/predict", json={k: v for k, v in good.items() if k != "날씨"}).json())
# JSON 이 아닌 본문, 없는 주소, 맞지 않는 메서드
print(client.post("/predict", content="주문", headers={"Content-Type": "application/json"}).status_code,
client.get("/predicts").status_code, client.get("/predict").status_code)칸이 빠짐: 422 · 지역 · Field required
타입이 틀림: 422 · 주문시각 · Input should be a valid integer, unable to parse string as an integer
범위를 넘음: 422 · 주문시각 · Input should be less than or equal to 23
없는 지역: 422 · 지역 · Input should be '서울', '경기', '인천' or '지방'
음수 무게: 422 · 무게kg · Input should be greater than or equal to 0
날씨 빠짐: {'지연확률': 0.361, '지연예상': False, '기준값': 0.5, '판': 'delay-lr-1'}
422 404 4053단계. 주문 200개를 묶음으로 예측하기
df = pd.read_csv("delivery.csv").drop_duplicates()
df = df[df["거리km"] < 9999].copy()
df["무게kg"] = df["무게kg"].fillna(df["무게kg"].median())
df["날씨"] = df["날씨"].fillna("모름")
cols = ["주문시각", "거리km", "무게kg", "지역", "상품분류", "날씨", "택배사"]
sample = df.sample(200, random_state=0)[cols]
r = client.post("/predict/batch", json={"주문들": sample.to_dict(orient="records")})
api_probs = [x["지연확률"] for x in r.json()["결과"]]
direct = joblib.load("out_delay_model.joblib")["파이프라인"].predict_proba(sample)[:, 1] # 모델을 바로 부른 값
print(r.status_code, "건수", r.json()["건수"], "· 가장 큰 차이", round(max(abs(a - b) for a, b in zip(api_probs, direct)), 6))
print("지연 예상", sum(x["지연예상"] for x in r.json()["결과"]), "건")200 건수 200 · 가장 큰 차이 5e-05
지연 예상 11 건4단계. 묶음의 한도 확인하기
too_many = {"주문들": [good] * 501} # 정한 한도(500개)를 넘는 묶음
r = client.post("/predict/batch", json=too_many)
print(r.status_code, r.json()["detail"][0]["msg"])
print(client.post("/predict/batch", json={"주문들": []}).status_code) # 빈 묶음422 List should have at most 500 items after validation, not 501
422이 실습이 뜻하는 것
이 실습은 창구 직원이 주문서를 주방에 넘기기 전에 빈칸과 틀린 칸을 걸러 내는 일과 같습니다. 실습 2처럼 창구를 열고 바른 주문서를 한 장 내 보고(1단계), 일부러 틀린 주문서 다섯 장과 엉뚱한 요청을 내 봅니다(2단계). 그다음 주문서 200장을 한 묶음으로 내고 주방에 직접 물은 값과 견주고(3단계), 너무 큰 묶음과 빈 묶음을 내 봅니다(4단계). 여기서 걸러 내는 직원이 Pydantic 검사, 주방이 모델, 되돌려 주는 쪽지가 422 응답의 detail, 묶음 주문이 /predict/batch 입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 바른 주문 | 200 | 규칙에 맞는 주문은 잘 처리됩니다 |
| 2. 틀린 다섯 | 모두 422, 예: 칸이 빠짐: 422 · 지역 · Field required | 칸이 빠지거나, 타입이나 범위가 틀리거나, 없는 지역이거나, 무게가 음수면 모델에 닿기 전에 막혔습니다. 어느 칸이 왜 틀렸는지 함께 알려 줍니다 |
| 2. 날씨 빠짐 | '지연확률': 0.361, '지연예상': False | 날씨는 기본값 "모름"이 있어 빠져도 예측됩니다 |
| 2. 그 밖 | 422 404 405 | JSON 이 아닌 본문은 422, 없는 주소는 404, 주소는 있지만 방식(GET)이 틀리면 405 입니다 |
| 3. 묶음 200개 | 200 건수 200 · 가장 큰 차이 5e-05 | API 의 확률과 모델을 바로 부른 확률이 가장 커도 0.00005 만 달랐습니다. API 가 넷째 자리로 반올림했기 때문입니다 |
| 3. 지연 예상 | 지연 예상 11 건 | 200건 가운데 11건(5.5%)을 지연으로 예상했습니다 |
| 4. 한도 | 422 List should have at most 500 items …, 422 | 501개 묶음과 빈 묶음은 모두 막혔습니다 |
한 줄로 줄이면, 입력 검사를 앞에 두면 잘못된 요청은 모델에 닿지 않고 422 와 고칠 곳을 돌려받으며, 묶음 예측은 한 건씩 부른 것과 같은 값을 낸다는 실습입니다. 실습 4에서는 진짜 서버로 띄워 한 건씩과 묶음의 시간을 견줍니다. 검사는 꼴과 범위만 봅니다. 규칙에 맞는 값이라도 예측이 맞는지는 보장하지 않습니다. 데이터의 지연 비율(0.206)에 비해 지연 예상(5.5%)이 적은 것도 실습 1의 낮은 재현율과 같은 이야기입니다.
코드 설명
- 2단계의 다섯 가지 잘못된 요청은 모두 모델에 닿기 전에 422 로 돌아왔습니다.
detail에 어느 칸(loc)이 왜(msg) 틀렸는지 적혀 있어, 부른 쪽이 무엇을 고칠지 알 수 있습니다. "부산"에는 "'서울', '경기', '인천' or '지방' 가운데 하나여야 한다"고 알려 줍니다. - 날씨를 빼면 기본값 "모름"으로 예측했습니다(0.361). JSON 이 아닌 본문은 422, 없는 주소는 404, 주소는 있지만 메서드가 다르면(
GET /predict) 405 입니다. - 3단계에서 주문 200개를 한 번에 보냈고, API 의 확률과 모델을 바로 부른 확률의 차이는 가장 커도 0.00005 였습니다. API 가 확률을 소수 넷째 자리로 반올림했기 때문이고, 계산은 같습니다. 200개 가운데 11개를 지연으로 예상했습니다.
Field(min_length=1, max_length=500)덕에 501개 묶음과 빈 묶음은 422 로 막혔습니다. 한도를 두지 않으면 아주 큰 요청 하나가 서버의 메모리와 시간을 다 차지할 수 있습니다.