1+ """
2+ Elementary ML nodes.
3+
4+ This module contains nodes for for machine learning workflows using sk-learn models.
5+ """
6+ import pandas as pd
7+ import numpy as np
8+
9+ from sklearn .model_selection import train_test_split
10+ from sklearn .ensemble import RandomForestRegressor
11+ from sklearn .linear_model import LinearRegression
12+ from sklearn .metrics import r2_score , mean_squared_error , mean_absolute_error
13+
14+ from core import as_function_node
15+
16+
17+ @as_function_node
18+ def MLDataSplitter (
19+ df ,
20+ y_name : str ,
21+ train_fraction : float = 0.70 ,
22+ validation_fraction : float = 0.15 ,
23+ test_fraction : float = 0.15 ,
24+ random_state : int = 42
25+ ):
26+ """
27+ Splits dataframe into train, validation, and test sets. This node prevents data leakage when connected correctly.
28+ using ONLY numeric feature columns.
29+ """
30+
31+ # -----------------------------
32+ # Validate fractions
33+ # -----------------------------
34+ total = train_fraction + validation_fraction + test_fraction
35+
36+ if not np .isclose (total , 1.0 ):
37+ raise ValueError ("Fractions must sum to 1.0" )
38+
39+ # -----------------------------
40+ # Remove missing rows
41+ # -----------------------------
42+ df = df .dropna ()
43+
44+ # -----------------------------
45+ # Target column
46+ # -----------------------------
47+ y = df [y_name ].copy ()
48+
49+ # -----------------------------
50+ # Feature columns
51+ # -----------------------------
52+ X_candidates = df .drop (columns = [y_name ])
53+
54+ # Keep ONLY numeric columns
55+ X_numeric = X_candidates .select_dtypes (include = ["number" ])
56+
57+ # -----------------------------
58+ # FIRST SPLIT
59+ # Train vs Temp
60+ # -----------------------------
61+ temp_fraction = validation_fraction + test_fraction
62+
63+ X_train , X_temp , y_train , y_temp = train_test_split (
64+ X_numeric ,
65+ y ,
66+ test_size = temp_fraction ,
67+ random_state = random_state
68+ )
69+
70+ # -----------------------------
71+ # SECOND SPLIT
72+ # Validation vs Test
73+ # -----------------------------
74+ validation_size_adjusted = validation_fraction / temp_fraction
75+
76+ X_validation , X_test , y_validation , y_test = train_test_split (
77+ X_temp ,
78+ y_temp ,
79+ test_size = (1 - validation_size_adjusted ),
80+ random_state = random_state
81+ )
82+
83+ return X_train , X_validation , X_test , y_train , y_validation , y_test
84+
85+
86+
87+ @as_function_node
88+ def train_regressor (X_train :pd .DataFrame , y_train :pd .DataFrame , r_type : str = None ):
89+ """
90+ trains a regressor
91+ """
92+ if r_type != None :
93+ if r_type == "linear" :
94+ reg = LinearRegression ().fit (X_train , y_train )
95+ if r_type == "tree" :
96+ reg = RandomForestRegressor ().fit (X_train , y_train )
97+ return reg
98+
99+
100+
101+
102+
103+ # =========================================================
104+ # 2) MODEL EVALUATION FUNCTION
105+ # =========================================================
106+
107+ @as_function_node
108+ def EvaluateRegressionModel (model , X_test , y_test ):
109+ """
110+ Evaluates a regression model.
111+
112+ Returns:
113+ R2
114+ MSE
115+ MAE
116+ """
117+
118+ # Predictions
119+ y_pred = model .predict (X_test )
120+
121+ # Metrics
122+ r2 = r2_score (y_test , y_pred )
123+ mse = mean_squared_error (y_test , y_pred )
124+ mae = mean_absolute_error (y_test , y_pred )
125+ out = {
126+ "R2" : r2 ,
127+ "MSE" : mse ,
128+ "MAE" : mae
129+ }
130+ return out
131+
132+
133+ # =========================================================
134+ # 3) MODEL COMPARISON FUNCTION
135+ # =========================================================
136+
137+
138+ @as_function_node
139+ def ChooseBestModel (
140+ model_1 ,
141+ model_2 ,
142+ X_validation ,
143+ y_validation
144+ ):
145+ """
146+ Compares two regression models on VALIDATION DATA.
147+
148+ Selection Priority:
149+ 1. Higher R2
150+ 2. Lower RMSE
151+
152+ Returns:
153+ best_model
154+ comparison_results
155+ """
156+
157+ # -----------------------------
158+ # Predictions
159+ # -----------------------------
160+ pred_1 = model_1 ["model" ].predict (X_validation )
161+ pred_2 = model_2 ["model" ].predict (X_validation )
162+
163+ # -----------------------------
164+ # Metrics for model 1
165+ # -----------------------------
166+ r2_1 = r2_score (y_validation , pred_1 )
167+ rmse_1 = np .sqrt (mean_squared_error (y_validation , pred_1 ))
168+
169+ # -----------------------------
170+ # Metrics for model 2
171+ # -----------------------------
172+ r2_2 = r2_score (y_validation , pred_2 )
173+ rmse_2 = np .sqrt (mean_squared_error (y_validation , pred_2 ))
174+
175+ # -----------------------------
176+ # Choose best model
177+ # -----------------------------
178+ if r2_1 > r2_2 :
179+ best_model = model_1
180+
181+ elif r2_2 > r2_1 :
182+ best_model = model_2
183+
184+ else :
185+ # If R2 tied -> lower RMSE wins
186+ if rmse_1 < rmse_2 :
187+ best_model = model_1
188+ else :
189+ best_model = model_2
190+
191+ results = {
192+ "model_1" : {
193+ "R2" : r2_1 ,
194+ "RMSE" : rmse_1
195+ },
196+ "model_2" : {
197+ "R2" : r2_2 ,
198+ "RMSE" : rmse_2
199+ }
200+ }
201+
202+ return best_model , results
0 commit comments