1010import pandas as pd
1111from fastapi import APIRouter , Depends , HTTPException , Request , UploadFile , File
1212from fastapi .responses import StreamingResponse
13- from sklearn .model_selection import train_test_split
1413from sqlalchemy .ext .asyncio import AsyncSession
1514from sqlalchemy import select
1615
@@ -80,92 +79,91 @@ async def start_training(
8079 raise HTTPException (status_code = 409 , detail = "Training already in progress for this project." )
8180
8281 try :
83- loop = asyncio .get_running_loop ()
84- _pipeline , task_type , metrics = await loop .run_in_executor (
85- None ,
86- partial (
87- train_model ,
88- df = df ,
89- model_type = body .model_type ,
90- target_column = body .target_column ,
91- test_size = body .test_size ,
92- hyperparameters = body .hyperparameters ,
93- task_type_override = body .task_type ,
94- ),
95- )
96- except ValueError as exc :
97- raise HTTPException (status_code = 400 , detail = str (exc ))
98- except Exception as exc :
99- logger .exception (f"Training failed for project { project_id } " )
100- raise HTTPException (status_code = 500 , detail = f"Training failed: { exc } " )
101- finally :
102- await release_training_lock (project_id )
103-
104- sanitized_metrics = sanitize_for_json (metrics )
105-
106- # Serialize the trained pipeline and persist it so export/predict don't re-train
107- model_id = uuid .uuid4 ()
108-
109- def _serialize ():
110- buf = io .BytesIO ()
111- joblib .dump (_pipeline , buf )
112- return buf .getvalue ()
113-
114- model_bytes = await loop .run_in_executor (None , _serialize )
115- model_stored = False
116- try :
117- await upload_model (str (model_id ), model_bytes )
118- model_stored = True
119- except Exception as storage_exc :
120- logger .error (f"Model storage failed for project { project_id } : { storage_exc } " )
121-
122- best_q = await db .execute (
123- select (TrainedModel ).where (
124- TrainedModel .project_id == parse_project_id (project_id ),
125- TrainedModel .is_best == True , # noqa: E712
82+ try :
83+ loop = asyncio .get_running_loop ()
84+ _pipeline , task_type , metrics = await loop .run_in_executor (
85+ None ,
86+ partial (
87+ train_model ,
88+ df = df ,
89+ model_type = body .model_type ,
90+ target_column = body .target_column ,
91+ test_size = body .test_size ,
92+ hyperparameters = body .hyperparameters ,
93+ task_type_override = body .task_type ,
94+ ),
95+ )
96+ except ValueError as exc :
97+ raise HTTPException (status_code = 400 , detail = str (exc ))
98+ except Exception as exc :
99+ logger .exception (f"Training failed for project { project_id } " )
100+ raise HTTPException (status_code = 500 , detail = f"Training failed: { exc } " )
101+
102+ sanitized_metrics = sanitize_for_json (metrics )
103+ model_id = uuid .uuid4 ()
104+
105+ def _serialize ():
106+ buf = io .BytesIO ()
107+ joblib .dump (_pipeline , buf )
108+ return buf .getvalue ()
109+
110+ model_bytes = await loop .run_in_executor (None , _serialize )
111+ model_stored = False
112+ try :
113+ await upload_model (str (model_id ), model_bytes )
114+ model_stored = True
115+ except Exception as storage_exc :
116+ logger .error (f"Model storage failed for project { project_id } : { storage_exc } " )
117+
118+ best_q = await db .execute (
119+ select (TrainedModel ).where (
120+ TrainedModel .project_id == parse_project_id (project_id ),
121+ TrainedModel .is_best == True , # noqa: E712
122+ )
126123 )
127- )
128- existing_bests = list (best_q .scalars ().all ())
129-
130- if existing_bests :
131- if task_type == "classification" :
132- best_existing = max ((m .metrics or {}).get ("accuracy" , 0.0 ) for m in existing_bests )
133- is_best = sanitized_metrics .get ("accuracy" , 0.0 ) >= best_existing
124+ existing_bests = list (best_q .scalars ().all ())
125+
126+ if existing_bests :
127+ if task_type == "classification" :
128+ best_existing = max ((m .metrics or {}).get ("accuracy" , 0.0 ) for m in existing_bests )
129+ is_best = sanitized_metrics .get ("accuracy" , 0.0 ) >= best_existing
130+ else :
131+ best_existing = max ((m .metrics or {}).get ("r2" , float ("-inf" )) for m in existing_bests )
132+ is_best = sanitized_metrics .get ("r2" , float ("-inf" )) >= best_existing
133+ if is_best :
134+ for m in existing_bests :
135+ m .is_best = False
134136 else :
135- best_existing = max ((m .metrics or {}).get ("r2" , float ("-inf" )) for m in existing_bests )
136- is_best = sanitized_metrics .get ("r2" , float ("-inf" )) >= best_existing
137- if is_best :
138- for m in existing_bests :
139- m .is_best = False
140- else :
141- is_best = True
142-
143- model_row = TrainedModel (
144- id = model_id ,
145- project_id = parse_project_id (project_id ),
146- model_type = body .model_type ,
147- target_column = body .target_column ,
148- task_type = task_type ,
149- metrics = sanitized_metrics ,
150- parameters = body .hyperparameters ,
151- is_best = is_best ,
152- model_path = str (model_id ) if model_stored else None ,
153- )
154- db .add (model_row )
137+ is_best = True
138+
139+ model_row = TrainedModel (
140+ id = model_id ,
141+ project_id = parse_project_id (project_id ),
142+ model_type = body .model_type ,
143+ target_column = body .target_column ,
144+ task_type = task_type ,
145+ metrics = sanitized_metrics ,
146+ parameters = body .hyperparameters ,
147+ is_best = is_best ,
148+ model_path = str (model_id ) if model_stored else None ,
149+ )
150+ db .add (model_row )
155151
156- project .current_step = "evaluation"
157- project .status = "completed"
158- await db .commit ()
152+ project .current_step = "evaluation"
153+ project .status = "completed"
154+ await db .commit ()
159155
160- logger .info (f"Training complete for project { project_id } : { body .model_type } ({ task_type } ) acc={ sanitized_metrics .get ('accuracy' )} " )
156+ logger .info (f"Training complete for project { project_id } : { body .model_type } ({ task_type } ) acc={ sanitized_metrics .get ('accuracy' )} " )
161157
162- return {
163- "success" : True ,
164- "model_type" : body .model_type ,
165- "target_column" : body .target_column ,
166- "task_type" : task_type ,
167- "metrics" : sanitized_metrics ,
168- }
158+ return {
159+ "success" : True ,
160+ "model_type" : body .model_type ,
161+ "target_column" : body .target_column ,
162+ "task_type" : task_type ,
163+ "metrics" : sanitized_metrics ,
164+ }
165+ finally :
166+ await release_training_lock (project_id )
169167
170168
171169@router .get ("/evaluate/{project_id}" , response_model = EvaluateResponse )
@@ -496,7 +494,7 @@ async def export_predictions(
496494 current_user : User = Depends (get_current_user ),
497495 db : AsyncSession = Depends (get_db ),
498496) -> StreamingResponse :
499- """Loads the stored model, runs inference on a test split, returns CSV with actual vs predicted."""
497+ """Loads the stored model and returns a CSV of actual vs predicted over the full dataset ."""
500498 best_model , df = await _get_best_model_and_df (project_id , current_user , db )
501499 loop = asyncio .get_running_loop ()
502500 pipe = await _load_pipeline (best_model , loop )
@@ -509,11 +507,9 @@ def _build_csv():
509507 if bool_cols :
510508 X = X .copy ()
511509 X [bool_cols ] = X [bool_cols ].astype (np .int8 )
512- _ , X_test , _ , y_test = train_test_split (X , y , test_size = 0.2 , random_state = 42 )
513- y_pred = pipe .predict (X_test )
514- out = X_test .copy ()
515- out ["actual" ] = y_test .values
516- out ["predicted" ] = y_pred
510+ out = X .copy ()
511+ out ["actual" ] = y .values
512+ out ["predicted" ] = pipe .predict (X )
517513 buf = io .StringIO ()
518514 out .to_csv (buf , index = False )
519515 return buf .getvalue ()
0 commit comments