anekazek's picture
Initial public upload: FastAPI + joblib inference bundle
aa5ea1b verified
Raw
History Blame Contribute Delete
6.12 kB
import json
import joblib
import numpy as np
import pandas as pd
from pathlib import Path
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from typing import Dict, Any
ARTIFACT_DIR = Path(__file__).resolve().parent
ml_assets = {}
class PredictRequest(BaseModel):
features: Dict[str, Any] = Field(..., description="Feature dict untuk 1 well/design row")
def encode_for_inference(
df_infer: pd.DataFrame,
trained_feature_columns,
encoded_category_maps,
) -> pd.DataFrame:
X = df_infer.copy()
# encode kategorikal sesuai mapping training
for col, cmap in encoded_category_maps.items():
if col in X.columns:
X[col] = X[col].map(cmap)
# unknown category -> -1
X[col] = X[col].fillna(-1)
# tambah kolom yang hilang
for col in trained_feature_columns:
if col not in X.columns:
X[col] = 0
# buang kolom ekstra, lalu reorder persis seperti training
X = X[trained_feature_columns].copy()
# paksa numerik
for col in X.columns:
X[col] = pd.to_numeric(X[col], errors="coerce").fillna(0)
return X
def enrich_derived_features(df: pd.DataFrame) -> pd.DataFrame:
out = df.copy()
# safe pressure limit
required_pressure_cols = [
"max_allowable_surface_pressure_psi",
"casing_pressure_limit_psi",
"tubing_pressure_limit_psi",
]
if all(c in out.columns for c in required_pressure_cols):
out["safe_pressure_limit_psi"] = out[required_pressure_cols].min(axis=1)
if "safe_pressure_limit_psi" in out.columns and "max_pressure_psi" in out.columns:
out["pressure_headroom_psi"] = out["safe_pressure_limit_psi"] - out["max_pressure_psi"]
if "avg_planned_rate_bpm" in out.columns and "max_pump_rate_bpm" in out.columns:
denom = out["max_pump_rate_bpm"].replace(0, np.nan)
out["rate_to_pump_capacity"] = (out["avg_planned_rate_bpm"] / denom).fillna(0)
if "inventory_proppant_ton" in out.columns and "total_planned_proppant_ton" in out.columns:
denom = out["total_planned_proppant_ton"].replace(0, np.nan)
out["inventory_proppant_coverage"] = (out["inventory_proppant_ton"] / denom).fillna(0)
if "inventory_fluid_bbl" in out.columns and "total_planned_fluid_bbl" in out.columns:
denom = out["total_planned_fluid_bbl"].replace(0, np.nan)
out["inventory_fluid_coverage"] = (out["inventory_fluid_bbl"] / denom).fillna(0)
if "total_planned_fluid_bbl" in out.columns and "total_planned_proppant_ton" in out.columns:
denom = out["total_planned_proppant_ton"].replace(0, np.nan)
out["fluid_to_proppant_ratio"] = (out["total_planned_fluid_bbl"] / denom).fillna(0)
if "planned_stage_count" in out.columns and "total_planned_proppant_ton" in out.columns:
denom = out["planned_stage_count"].replace(0, np.nan)
out["proppant_per_stage_ton"] = (out["total_planned_proppant_ton"] / denom).fillna(0)
if "planned_stage_count" in out.columns and "total_planned_fluid_bbl" in out.columns:
denom = out["planned_stage_count"].replace(0, np.nan)
out["fluid_per_stage_bbl"] = (out["total_planned_fluid_bbl"] / denom).fillna(0)
if "lateral_treated_length_m" in out.columns and "planned_stage_count" in out.columns:
denom = out["planned_stage_count"].replace(0, np.nan)
out["derived_stage_length_m"] = (out["lateral_treated_length_m"] / denom).fillna(0)
return out
@asynccontextmanager
async def lifespan(app: FastAPI):
with open(ARTIFACT_DIR / "artifact_manifest.json", "r", encoding="utf-8") as f:
manifest = json.load(f)
ml_assets["manifest"] = manifest
ml_assets["uplift_model"] = joblib.load(ARTIFACT_DIR / "uplift_model.joblib")
ml_assets["cost_model"] = joblib.load(ARTIFACT_DIR / "cost_model.joblib")
ml_assets["risk_model"] = joblib.load(ARTIFACT_DIR / "risk_model.joblib")
yield
ml_assets.clear()
app = FastAPI(
title="TRIDENT Design Recommender Inference API",
version="1.0.0",
lifespan=lifespan,
)
@app.get("/health")
def health():
return {
"status": "ok",
"artifacts_loaded": bool(ml_assets),
"model_version": ml_assets.get("manifest", {}).get("artifact_version"),
}
@app.post("/predict")
def predict(req: PredictRequest):
try:
manifest = ml_assets["manifest"]
trained_feature_columns = manifest["trained_feature_columns"]
encoded_category_maps = manifest["encoded_category_maps"]
default_eval_days = manifest["default_eval_days"]
risk_penalty_usd = manifest["risk_penalty_usd"]
df = pd.DataFrame([req.features])
df = enrich_derived_features(df)
X = encode_for_inference(
df_infer=df,
trained_feature_columns=trained_feature_columns,
encoded_category_maps=encoded_category_maps,
)
uplift = float(ml_assets["uplift_model"].predict(X)[0])
cost = float(ml_assets["cost_model"].predict(X)[0])
risk = float(ml_assets["risk_model"].predict_proba(X)[0, 1])
oil_price = float(df["realized_oil_price_usd_bbl"].iloc[0]) if "realized_oil_price_usd_bbl" in df.columns else 65.0
gross_value = uplift * default_eval_days * oil_price
design_score = gross_value - cost - risk * risk_penalty_usd
response = {
"pred_uplift_bopd": uplift,
"pred_cost_usd": cost,
"pred_screenout_risk": risk,
"pred_gross_value_usd": gross_value,
"pred_design_score_usd": design_score,
}
# optional info untuk debugging
if "safe_pressure_limit_psi" in df.columns:
response["safe_pressure_limit_psi"] = float(df["safe_pressure_limit_psi"].iloc[0])
if "pressure_headroom_psi" in df.columns:
response["pressure_headroom_psi"] = float(df["pressure_headroom_psi"].iloc[0])
return response
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))