Amaro2a commited on
Commit
f3fe1bb
·
verified ·
1 Parent(s): 3a49bae

Update app.py

Browse files

merge the csv preditions in response of an batch request

Files changed (1) hide show
  1. app.py +61 -60
app.py CHANGED
@@ -1,8 +1,7 @@
1
- # main.py
2
  import io
3
  import os
4
- from typing import List, Optional
5
- import joblib
6
  import numpy as np
7
  import pandas as pd
8
  import torch
@@ -10,12 +9,12 @@ import torch.nn as nn
10
  from fastapi import FastAPI, File, UploadFile, HTTPException
11
  from fastapi.middleware.cors import CORSMiddleware
12
  from pydantic import BaseModel
13
- from safetensors.torch import load_file
14
 
15
  # -----------------------------
16
  # Config
17
  # -----------------------------
18
- FEATURES = [
19
  "ESS_TOTAL", "MCATOT", "GDS_TOTAL",
20
  "MCAALTTM", "MCACUBE", "MCASER7", "MCAABSTR",
21
  "GDSSATIS", "GDSHAPPY", "GDSENRGY",
@@ -30,10 +29,9 @@ TARGETS = [
30
  ]
31
 
32
  MODEL_PATHS = {
33
- "jit": "mode/pd_model.ts",
34
- "state": "mode/pd_model.safetensors",
35
- "sx": "model/scaler_x.pkl",
36
- "sy": "model/scaler_y.pkl",
37
  }
38
 
39
  # -----------------------------
@@ -43,21 +41,21 @@ app = FastAPI(title="PD Biomarker Predictor", version="1.0.0")
43
 
44
  app.add_middleware(
45
  CORSMiddleware,
46
- allow_origins=["*"], # tighten for prod
47
  allow_credentials=True,
48
  allow_methods=["*"],
49
  allow_headers=["*"],
50
  )
51
 
52
  # -----------------------------
53
- # Model definition (for state_dict load)
54
  # -----------------------------
55
- class flake_Transformer(nn.Module):
56
  def __init__(self, num_features: int, output_dim: int, embed_dim=64, nhead=4, num_layers=3):
57
  super().__init__()
58
  self.feature_embeds = nn.ModuleList([nn.Linear(1, embed_dim) for _ in range(num_features)])
59
  encoder_layer = nn.TransformerEncoderLayer(
60
- d_model=embed_dim, nhead=nhead, dim_feedforward=256, dropout=0.1, batch_first=False
61
  )
62
  self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
63
  self.pool = nn.AdaptiveAvgPool1d(1)
@@ -70,46 +68,51 @@ class flake_Transformer(nn.Module):
70
 
71
  def forward(self, x):
72
  embeds = [self.feature_embeds[i](x[:, i].unsqueeze(1)) for i in range(x.shape[1])]
73
- x = torch.stack(embeds, dim=1) # [B, F, E]
74
- x = x.permute(1, 0, 2) # [F, B, E]
75
- x = self.transformer(x) # [F, B, E]
76
- x = x.permute(1, 2, 0) # [B, E, F]
77
- x = self.pool(x).squeeze(2) # [B, E]
78
- x = self.fc(x) # [B, 4]
79
  return x
80
 
81
  # -----------------------------
82
  # Load model and scalers
83
  # -----------------------------
84
  def load_model_and_scalers():
85
- # Scalers
86
- if not (os.path.exists(MODEL_PATHS["sx"]) and os.path.exists(MODEL_PATHS["sy"])):
87
- raise RuntimeError("Missing scalers. Expected scaler_x.pkl and scaler_y.pkl in mode/ directory.")
88
- scaler_x = joblib.load(MODEL_PATHS["sx"])
89
- scaler_y = joblib.load(MODEL_PATHS["sy"])
 
 
90
 
91
- # Model
92
- model = None
93
- if os.path.exists(MODEL_PATHS["jit"]):
94
- model = torch.jit.load(MODEL_PATHS["jit"], map_location="cpu")
95
- elif os.path.exists(MODEL_PATHS["state"]):
96
- # Load safetensors
 
 
 
97
  state_dict = load_file(MODEL_PATHS["state"], device="cpu")
98
- model = PDTabTransformer(num_features=len(FEATURES), output_dim=len(TARGETS))
99
  model.load_state_dict(state_dict)
100
- else:
101
- raise RuntimeError("Model file not found. Provide pd_model.ts or pd_model.safetensors")
102
- model.eval()
 
 
103
  return model, scaler_x, scaler_y
104
 
105
  MODEL, SCALER_X, SCALER_Y = load_model_and_scalers()
106
 
107
-
108
  # -----------------------------
109
  # Utilities
110
  # -----------------------------
111
  def try_compute_ess_total(df: pd.DataFrame) -> pd.DataFrame:
112
- # If ESS_TOTAL missing, sum ESS1..ESS8 when available
113
  if "ESS_TOTAL" not in df.columns:
114
  ess_cols = [f"ESS{i}" for i in range(1, 9) if f"ESS{i}" in df.columns]
115
  if len(ess_cols) == 8:
@@ -117,11 +120,10 @@ def try_compute_ess_total(df: pd.DataFrame) -> pd.DataFrame:
117
  return df
118
 
119
  def try_compute_gds_total(df: pd.DataFrame) -> pd.DataFrame:
120
- # If GDS_TOTAL missing, sum standard 15 items when available
121
  gds_items = [
122
- "GDSSATIS","GDSDROPD","GDSEMPTY","GDSBORED","GDSGSPIR",
123
- "GDSAFRAD","GDSHAPPY","GDSHLPLS","GDSHOME","GDSMEMRY",
124
- "GDSALIVE","GDSWRTLS","GDSENRGY","GDSHOPLS","GDSBETER"
125
  ]
126
  if "GDS_TOTAL" not in df.columns:
127
  present = [c for c in gds_items if c in df.columns]
@@ -139,11 +141,9 @@ def standardize_and_predict(df_features: pd.DataFrame):
139
  return preds
140
 
141
  def merge_four_frames(ess: pd.DataFrame, moca: pd.DataFrame, gds: pd.DataFrame, dat: pd.DataFrame) -> pd.DataFrame:
142
- # compute totals if needed
143
  ess = try_compute_ess_total(ess)
144
  gds = try_compute_gds_total(gds)
145
 
146
- # basic checks
147
  for df, name in [(ess, "ESS"), (moca, "MoCA"), (gds, "GDS"), (dat, "DaTSCAN")]:
148
  if not all(col in df.columns for col in ["PATNO", "EVENT_ID"]):
149
  raise HTTPException(status_code=400, detail=f"{name} CSV missing PATNO or EVENT_ID columns")
@@ -152,7 +152,6 @@ def merge_four_frames(ess: pd.DataFrame, moca: pd.DataFrame, gds: pd.DataFrame,
152
  df = df.merge(gds, on=["PATNO", "EVENT_ID"], how="inner")
153
  df = df.merge(dat, on=["PATNO", "EVENT_ID"], how="inner")
154
 
155
- # validate features present / inferable
156
  missing = [f for f in FEATURES if f not in df.columns]
157
  if missing:
158
  raise HTTPException(status_code=400, detail=f"Merged CSVs missing required features: {missing}")
@@ -160,9 +159,8 @@ def merge_four_frames(ess: pd.DataFrame, moca: pd.DataFrame, gds: pd.DataFrame,
160
  return df
161
 
162
  def detect_file_kind(name: str) -> str:
163
- """Rudimentary detector to map filename to dataset kind."""
164
  l = name.lower()
165
- if "datscan" in l or "dat" in l and "scan" in l:
166
  return "datscan"
167
  if "moca" in l:
168
  return "moca"
@@ -213,7 +211,6 @@ async def predict_files(files: List[UploadFile] = File(...)):
213
  if len(files) < 4:
214
  raise HTTPException(status_code=400, detail="Please upload four CSV files: ESS, MoCA, GDS, DaTSCAN.")
215
 
216
- # Read all
217
  buckets = {"ess": None, "moca": None, "gds": None, "datscan": None}
218
  fallback = []
219
  for f in files:
@@ -229,7 +226,6 @@ async def predict_files(files: List[UploadFile] = File(...)):
229
  else:
230
  fallback.append((kind, df))
231
 
232
- # If detection failed, try to auto-assign remaining by column heuristics
233
  if buckets["ess"] is None:
234
  candidates = [df for kind, df in fallback if "ESS1" in df.columns or "ESS_TOTAL" in df.columns]
235
  if candidates:
@@ -251,20 +247,25 @@ async def predict_files(files: List[UploadFile] = File(...)):
251
  raise HTTPException(status_code=400, detail="Could not identify all four CSVs (ESS, MoCA, GDS, DaTSCAN) by filename/columns.")
252
 
253
  merged = merge_four_frames(buckets["ess"], buckets["moca"], buckets["gds"], buckets["datscan"])
254
-
255
- # Predict for all rows; return the first plus summary
256
  preds = standardize_and_predict(merged)
257
- first = preds[0].tolist()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
  return {
260
- "predicted_biomarkers": {
261
- TARGETS[0]: first[0],
262
- TARGETS[1]: first[1],
263
- TARGETS[2]: first[2],
264
- TARGETS[3]: first[3],
265
- },
266
  "source": "files",
267
- "merged_rows": int(merged.shape[0]),
268
- "note": "Multiple rows detected; the first row's prediction is returned. Consider filtering by PATNO/EVENT_ID for per-subject results."
269
- }
270
-
 
 
1
  import io
2
  import os
3
+ from typing import List
4
+ import pickle
5
  import numpy as np
6
  import pandas as pd
7
  import torch
 
9
  from fastapi import FastAPI, File, UploadFile, HTTPException
10
  from fastapi.middleware.cors import CORSMiddleware
11
  from pydantic import BaseModel
12
+ from safetensors.torch import load_file
13
 
14
  # -----------------------------
15
  # Config
16
  # -----------------------------
17
+ FEATUREacieS = [
18
  "ESS_TOTAL", "MCATOT", "GDS_TOTAL",
19
  "MCAALTTM", "MCACUBE", "MCASER7", "MCAABSTR",
20
  "GDSSATIS", "GDSHAPPY", "GDSENRGY",
 
29
  ]
30
 
31
  MODEL_PATHS = {
32
+ "state": "model/flake_transformer.safetensors",
33
+ "sx": "model/scaler_x.pkl",
34
+ "sy": "model/scaler_y.pkl",
 
35
  }
36
 
37
  # -----------------------------
 
41
 
42
  app.add_middleware(
43
  CORSMiddleware,
44
+ allow_origins=["*"], # Tighten for production
45
  allow_credentials=True,
46
  allow_methods=["*"],
47
  allow_headers=["*"],
48
  )
49
 
50
  # -----------------------------
51
+ # Model definition
52
  # -----------------------------
53
+ class flakeParkinsonTransformer(nn.Module):
54
  def __init__(self, num_features: int, output_dim: int, embed_dim=64, nhead=4, num_layers=3):
55
  super().__init__()
56
  self.feature_embeds = nn.ModuleList([nn.Linear(1, embed_dim) for _ in range(num_features)])
57
  encoder_layer = nn.TransformerEncoderLayer(
58
+ d_model=embed_dim, nhead=nhead, dim_feedforward=256, dropout=0.1
59
  )
60
  self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
61
  self.pool = nn.AdaptiveAvgPool1d(1)
 
68
 
69
  def forward(self, x):
70
  embeds = [self.feature_embeds[i](x[:, i].unsqueeze(1)) for i in range(x.shape[1])]
71
+ x = torch.stack(embeds, dim=1)
72
+ x = x.permute(1, 0, 2)
73
+ x = self.transformer(x)
74
+ x = x.permute(1, 2, 0)
75
+ x = self.pool(x).squeeze(2)
76
+ x = self.fc(x)
77
  return x
78
 
79
  # -----------------------------
80
  # Load model and scalers
81
  # -----------------------------
82
  def load_model_and_scalers():
83
+ # Load scalers using pickle
84
+ try:
85
+ with open(MODEL_PATHS["sx"], 'rb') as f:
86
+ scaler_x = pickle.load(f)
87
+ print("scaler_x loaded successfully.")
88
+ except Exception as e:
89
+ raise RuntimeError(f"Error loading scaler_x: {e}")
90
 
91
+ try:
92
+ with open(MODEL_PATHS["sy"], 'rb') as f:
93
+ scaler_y = pickle.load(f)
94
+ print("scaler_y loaded successfully.")
95
+ except Exception as e:
96
+ raise RuntimeError(f"Error loading scaler_y: {e}")
97
+
98
+ # Load model using safetensors
99
+ try:
100
  state_dict = load_file(MODEL_PATHS["state"], device="cpu")
101
+ model = flakeParkinsonTransformer(num_features=len(FEATURES), output_dim=len(TARGETS))
102
  model.load_state_dict(state_dict)
103
+ model.eval()
104
+ print("Model loaded and set to evaluation mode.")
105
+ except Exception as e:
106
+ raise RuntimeError(f"Error loading model state dictionary: {e}")
107
+
108
  return model, scaler_x, scaler_y
109
 
110
  MODEL, SCALER_X, SCALER_Y = load_model_and_scalers()
111
 
 
112
  # -----------------------------
113
  # Utilities
114
  # -----------------------------
115
  def try_compute_ess_total(df: pd.DataFrame) -> pd.DataFrame:
 
116
  if "ESS_TOTAL" not in df.columns:
117
  ess_cols = [f"ESS{i}" for i in range(1, 9) if f"ESS{i}" in df.columns]
118
  if len(ess_cols) == 8:
 
120
  return df
121
 
122
  def try_compute_gds_total(df: pd.DataFrame) -> pd.DataFrame:
 
123
  gds_items = [
124
+ "GDSSATIS", "GDSDROPD", "GDSEMPTY", "GDSBORED", "GDSGSPIR",
125
+ "GDSAFRAD", "GDSHAPPY", "GDSHLPLS", "GDSHOME", "GDSMEMRY",
126
+ "GDSALIVE", "GDSWRTLS", "GDSENRGY", "GDSHOPLS", "GDSBETER"
127
  ]
128
  if "GDS_TOTAL" not in df.columns:
129
  present = [c for c in gds_items if c in df.columns]
 
141
  return preds
142
 
143
  def merge_four_frames(ess: pd.DataFrame, moca: pd.DataFrame, gds: pd.DataFrame, dat: pd.DataFrame) -> pd.DataFrame:
 
144
  ess = try_compute_ess_total(ess)
145
  gds = try_compute_gds_total(gds)
146
 
 
147
  for df, name in [(ess, "ESS"), (moca, "MoCA"), (gds, "GDS"), (dat, "DaTSCAN")]:
148
  if not all(col in df.columns for col in ["PATNO", "EVENT_ID"]):
149
  raise HTTPException(status_code=400, detail=f"{name} CSV missing PATNO or EVENT_ID columns")
 
152
  df = df.merge(gds, on=["PATNO", "EVENT_ID"], how="inner")
153
  df = df.merge(dat, on=["PATNO", "EVENT_ID"], how="inner")
154
 
 
155
  missing = [f for f in FEATURES if f not in df.columns]
156
  if missing:
157
  raise HTTPException(status_code=400, detail=f"Merged CSVs missing required features: {missing}")
 
159
  return df
160
 
161
  def detect_file_kind(name: str) -> str:
 
162
  l = name.lower()
163
+ if "datscan" in l or ("dat" in l and "scan" in l):
164
  return "datscan"
165
  if "moca" in l:
166
  return "moca"
 
211
  if len(files) < 4:
212
  raise HTTPException(status_code=400, detail="Please upload four CSV files: ESS, MoCA, GDS, DaTSCAN.")
213
 
 
214
  buckets = {"ess": None, "moca": None, "gds": None, "datscan": None}
215
  fallback = []
216
  for f in files:
 
226
  else:
227
  fallback.append((kind, df))
228
 
 
229
  if buckets["ess"] is None:
230
  candidates = [df for kind, df in fallback if "ESS1" in df.columns or "ESS_TOTAL" in df.columns]
231
  if candidates:
 
247
  raise HTTPException(status_code=400, detail="Could not identify all four CSVs (ESS, MoCA, GDS, DaTSCAN) by filename/columns.")
248
 
249
  merged = merge_four_frames(buckets["ess"], buckets["moca"], buckets["gds"], buckets["datscan"])
 
 
250
  preds = standardize_and_predict(merged)
251
+
252
+ # Create a list of predictions for each patient
253
+ results = []
254
+ for idx, pred in enumerate(preds):
255
+ result = {
256
+ "PATNO": int(merged.iloc[idx]["PATNO"]),
257
+ "EVENT_ID": str(merged.iloc[idx]["EVENT_ID"]),
258
+ "predicted_biomarkers": {
259
+ TARGETS[0]: float(pred[0]),
260
+ TARGETS[1]: float(pred[1]),
261
+ TARGETS[2]: float(pred[2]),
262
+ TARGETS[3]: float(pred[3]),
263
+ }
264
+ }
265
+ results.append(result)
266
 
267
  return {
268
+ "predictions": results,
 
 
 
 
 
269
  "source": "files",
270
+ "merged_rows": int(merged.shape[0])
271
+ }