3
3
import sqlite3 , pandas
4
4
from rich import print
5
5
from pathlib import Path
6
+ from scipy import stats
6
7
7
8
8
9
class MetricDB :
@@ -53,9 +54,7 @@ def get_moving_average(self, key: str, name_table: str = "main", window_size: in
53
54
return moving_average
54
55
55
56
if self .verbose :
56
- print (
57
- f"[bold yellow]Warning: No valid numeric values found for key '{ key } ' in table '{ name_table } '. Returning 0.[/bold yellow]"
58
- )
57
+ print (f"[bold yellow]Warning: No valid numeric values found for key '{ key } ' in table '{ name_table } '. Returning 0.[/bold yellow]" )
59
58
return 0
60
59
61
60
def log (self , data : dict , name_table : str = "main" ):
@@ -161,9 +160,7 @@ def get_dataframe(self, name_table: str = "main"):
161
160
try :
162
161
# Check if column contains bytes that should be treated as numbers
163
162
if df [column ].dtype == object and any (isinstance (x , bytes ) for x in df [column ].dropna ()):
164
- df [column ] = df [column ].apply (
165
- lambda x : int .from_bytes (x , byteorder = "little" ) if isinstance (x , bytes ) else x
166
- )
163
+ df [column ] = df [column ].apply (lambda x : int .from_bytes (x , byteorder = "little" ) if isinstance (x , bytes ) else x )
167
164
continue
168
165
169
166
numeric_series = pandas .to_numeric (df [column ], errors = "raise" )
@@ -272,6 +269,51 @@ def _write_dummy_data(self, name_table: str = "dummy_table"):
272
269
print (f"[bold green]Dummy data inserted successfully into { name_table } ![/bold green]" )
273
270
self .print_header ()
274
271
272
+ def has_plateaued (
273
+ self ,
274
+ key : str ,
275
+ name_table : str = "main" ,
276
+ window_size : int = 16 ,
277
+ threshold_p : float = 0.05 ,
278
+ threshold_slope : float = 0.03 ,
279
+ ):
280
+ """check if the key has plateaued in the past window_size values / has to be statistically significant"""
281
+ cursor = self .connect .cursor ()
282
+ cursor .execute (f"PRAGMA table_info({ name_table } )" )
283
+ columns = [col [1 ] for col in cursor .fetchall ()]
284
+
285
+ if key not in columns :
286
+ if self .verbose :
287
+ print (f"[bold yellow]Warning: Key '{ key } ' does not exist in table '{ name_table } '. Returning False.[/bold yellow]" )
288
+ return False
289
+
290
+ query = f"""
291
+ SELECT "{ key } "
292
+ FROM { name_table }
293
+ WHERE "{ key } " IS NOT NULL
294
+ ORDER BY id DESC
295
+ LIMIT { window_size }
296
+ """
297
+
298
+ cursor .execute (query )
299
+ results = cursor .fetchall ()
300
+ cursor .close ()
301
+ if not results :
302
+ if self .verbose :
303
+ print (f"[bold yellow]Warning: No data found for key '{ key } ' in table '{ name_table } '. Returning False.[/bold yellow]" )
304
+ return False
305
+ values = [float (result [0 ]) for result in results if result [0 ] is not None ][- window_size :]
306
+ if len (values ) < 8 :
307
+ if self .verbose :
308
+ print (f"[bold yellow]Warning: Not enough valid data points for key '{ key } '. Returning False.[/bold yellow]" )
309
+ return False
310
+ x = list (range (len (values )))
311
+ slope , _ , _ , p_value , _ = stats .linregress (x , values )
312
+ is_flat = p_value > threshold_p and abs (slope ) < threshold_slope
313
+ if self .verbose and is_flat :
314
+ print (f"[bold green]Values for '{ key } ' have plateaued (p = { p_value :.4f} )[/bold green]" )
315
+ return is_flat
316
+
275
317
276
318
if __name__ == "__main__" :
277
319
# logger = MetricDB("default.db", verbose=True)
@@ -290,14 +332,48 @@ def _write_dummy_data(self, name_table: str = "dummy_table"):
290
332
# logger.show_last_row()
291
333
# logger.on_end()
292
334
293
- logger = MetricDB ("default.db" , verbose = True )
294
- logger .print_header ()
295
- # logger.get_moving_average(key="Valid Accuracy Balanced")
335
+ # logger = MetricDB("default.db", verbose=True)
336
+ # logger.print_header()
337
+ # # logger.get_moving_average(key="Valid Accuracy Balanced")
338
+
339
+ # # ---- [1] Demo numeric encoding issue ----
340
+ # logger.log({"age": 25.1})
341
+ # logger.log({"name": "Alice"})
342
+ # df = logger.get_dataframe()
343
+ # # ---- [2] All conversions are handled automatically ----
344
+ # print(df)
345
+ # logger.on_end()
346
+
347
+ # Test has_plateaued with synthetic data using known functions
348
+ logger = MetricDB ("test_plateau.db" , verbose = True )
349
+
350
+ # Test case 1: Logistic function (natural plateau)
351
+ # f(x) = L / (1 + e^(-k(x-x0))) where L=1, k=1, x0=5
352
+ def logistic (x ):
353
+ return 1 / (1 + 2.71828 ** (- 1 * (x - 5 )))
354
+
355
+ print ("\n Testing logistic function plateau:" )
356
+ for x in range (64 ):
357
+ logger .log ({"logistic_metric" : logistic (x )})
358
+ is_plateau = logger .has_plateaued ("logistic_metric" )
359
+ print (f"Has plateaued: { is_plateau } " )
360
+
361
+ # Test case 2: Exponential decay (asymptotic plateau)
362
+ # f(x) = e^(-0.5x)
363
+ logger = MetricDB ("test_exp.db" , verbose = True )
364
+ print ("\n Testing exponential decay plateau:" )
365
+ for x in range (64 ):
366
+ logger .log ({"exp_metric" : 2.71828 ** (- 0.5 * x )})
367
+ is_plateau = logger .has_plateaued ("exp_metric" )
368
+ print (f"Has plateaued: { is_plateau } " )
369
+
370
+ # Test case 3: Linear function (no plateau)
371
+ # f(x) = 0.5x
372
+ logger = MetricDB ("test_linear.db" , verbose = True )
373
+ print ("\n Testing linear function (should not plateau):" )
374
+ for x in range (64 ):
375
+ logger .log ({"linear_metric" : 0.5 * x })
376
+ is_plateau = logger .has_plateaued ("linear_metric" )
377
+ print (f"Has plateaued: { is_plateau } " )
296
378
297
- # ---- [1] Demo numeric encoding issue ----
298
- logger .log ({"age" : 25.1 })
299
- logger .log ({"name" : "Alice" })
300
- df = logger .get_dataframe ()
301
- # ---- [2] All conversions are handled automatically ----
302
- print (df )
303
379
logger .on_end ()
0 commit comments