1+ import logging
2+
13import numpy as np
24import pandas as pd
35import plotly .graph_objs as go
46from nicegui import run , ui
57from sklearn .cluster import DBSCAN , AgglomerativeClustering , KMeans
6- from sklearn .datasets import load_digits , load_iris
8+ from sklearn .datasets import fetch_openml , load_digits , load_iris
79from sklearn .decomposition import PCA
810from umap import UMAP
911
1214from tdamapper .learn import MapperAlgorithm
1315from tdamapper .plot import MapperPlot
1416
17+ logger = logging .getLogger (__name__ )
18+ logging .basicConfig (level = logging .INFO )
19+
1520
1621def mode (arr ):
1722 values , counts = np .unique (arr , return_counts = True )
@@ -20,6 +25,14 @@ def mode(arr):
2025 return np .nanmean (mode_values )
2126
2227
28+ def _fix_data (data ):
29+ df = pd .DataFrame (data )
30+ df = df .select_dtypes (include = "number" )
31+ df .dropna (axis = 1 , how = "all" , inplace = True )
32+ df .fillna (df .mean (), inplace = True )
33+ return df
34+
35+
2336def _identity (X ):
2437 return X
2538
@@ -66,6 +79,9 @@ def _func(X):
6679DRAW_3D = "3D"
6780DRAW_2D = "2D"
6881DRAW_ITERATIONS = 50
82+ DRAW_MEAN = "Mean"
83+ DRAW_MEDIAN = "Median"
84+ DRAW_MODE = "Mode"
6985
7086
7187class App :
@@ -96,7 +112,9 @@ def build_dataset(self):
96112 value = DATA_SOURCE_EXAMPLE ,
97113 )
98114 self .data_source_csv = ui .upload (
99- on_upload = self .update_dataset_handler ,
115+ on_upload = self .update_csv_handler ,
116+ auto_upload = True ,
117+ label = "Upload CSV" ,
100118 ).classes ("w-full" )
101119 self .data_source_csv .bind_visibility_from (
102120 target_object = self .data_source_type ,
@@ -278,102 +296,166 @@ def build_draw(self):
278296 value = DRAW_ITERATIONS ,
279297 on_change = self .update_plot_handler ,
280298 )
299+ self .draw_aggregation = ui .select (
300+ label = "Aggregation" ,
301+ options = [
302+ DRAW_MEAN ,
303+ DRAW_MEDIAN ,
304+ DRAW_MODE ,
305+ ],
306+ value = DRAW_MEAN ,
307+ on_change = self .update_plot_handler ,
308+ )
281309
282310 def build_plot (self ):
283311 fig = go .Figure ()
284312 fig .layout .width = None
285313 fig .layout .autosize = True
286314 self .plot_container = ui .element ("div" ).classes ("w-full h-full" )
287- with self .plot_container :
288- ui .plotly (go .Figure ())
289315
290316 def render_dataset (self ):
291317 source_type = self .data_source_type .value
292318 if source_type == DATA_SOURCE_EXAMPLE :
293319 name = self .data_source_example_file .value
294320 if name == DATA_SOURCE_EXAMPLE_DIGITS :
295- X , y = load_digits (return_X_y = True , as_frame = True )
296- return X , y
321+ df_X , df_y = load_digits (return_X_y = True , as_frame = True )
297322 elif name == DATA_SOURCE_EXAMPLE_IRIS :
298- X , y = load_iris (return_X_y = True , as_frame = True )
299- return X , y
323+ df_X , df_y = load_iris (return_X_y = True , as_frame = True )
300324 elif source_type == DATA_SOURCE_CSV :
301- pass
325+ csv_file = self .csv_file
326+ if csv_file is None :
327+ logger .warning ("No CSV file uploaded" )
328+ df_X , df_y = pd .DataFrame (), pd .Series ()
329+ else :
330+ df_X = pd .read_csv (csv_file .content )
331+ df_y = pd .Series ()
332+ elif source_type == DATA_SOURCE_OPENML :
333+ code = self .data_source_openml .value
334+ if not code :
335+ logger .warning ("No OpenML code provided" )
336+ df_X , df_y = pd .DataFrame (), pd .Series ()
337+ else :
338+ df_X , df_y = fetch_openml (code , return_X_y = True , as_frame = True )
339+ df_X = _fix_data (df_X )
340+ df_y = _fix_data (df_y )
341+ return df_X , df_y
302342
303343 def render_lens (self ):
304344 if self .lens_type .value == LENS_IDENTITY :
305345 return _identity
306346 elif self .lens_type .value == LENS_PCA :
307- n = int (self .pca_n_components .value )
347+ n = 2
348+ if self .pca_n_components .value is not None :
349+ n = int (self .pca_n_components .value )
308350 return _pca (n )
309351 elif self .lens_type .value == LENS_UMAP :
310- n = int (self .umap_n_components .value )
352+ n = 2
353+ if self .umap_n_components .value is not None :
354+ n = int (self .umap_n_components .value )
311355 return _umap (n )
312356
313357 def render_cover (self ):
314358 if self .cover_type .value == COVER_TRIVIAL :
315359 return TrivialCover ()
316360 elif self .cover_type .value == COVER_BALL :
317- radius = float (self .cover_ball_radius .value )
361+ radius = 1.0
362+ if self .cover_ball_radius .value is not None :
363+ radius = float (self .cover_ball_radius .value )
318364 return BallCover (radius = radius )
319365 elif self .cover_type .value == COVER_CUBICAL :
320- n_intervals = int (self .cover_cubical_n_intervals .value )
321- overlap_frac = float (self .cover_cubical_overlap_frac .value )
366+ n_intervals = 1
367+ if self .cover_cubical_n_intervals .value is not None :
368+ n_intervals = int (self .cover_cubical_n_intervals .value )
369+ overlap_frac = None
370+ if self .cover_cubical_overlap_frac .value is not None :
371+ overlap_frac = float (self .cover_cubical_overlap_frac .value )
322372 return CubicalCover (n_intervals = n_intervals , overlap_frac = overlap_frac )
323373 elif self .cover_type .value == COVER_KNN :
324- neighbors = int (self .cover_knn_neighbors .value )
374+ neighbors = 1
375+ if self .cover_knn_neighbors .value is not None :
376+ neighbors = int (self .cover_knn_neighbors .value )
325377 return KNNCover (neighbors = neighbors )
326378
327379 def render_clustering (self ):
380+ clustering_type = self .clustering_type .value
328381 if self .clustering_type .value == CLUSTERING_TRIVIAL :
329382 return TrivialClustering ()
330- elif self .clustering_type .value == CLUSTERING_KMEANS :
331- n_clusters = int (self .clustering_kmeans_n_clusters .value )
383+ elif clustering_type == CLUSTERING_KMEANS :
384+ n_clusters = 1
385+ if self .clustering_kmeans_n_clusters .value is not None :
386+ n_clusters = int (self .clustering_kmeans_n_clusters .value )
332387 return KMeans (n_clusters )
333- elif self .clustering_type .value == CLUSTERING_DBSCAN :
334- eps = float (self .clustering_dbscan_eps .value )
335- min_samples = int (self .clustering_dbscan_min_samples .value )
388+ elif clustering_type == CLUSTERING_DBSCAN :
389+ eps = 0.5
390+ if self .clustering_dbscan_eps .value is not None :
391+ eps = float (self .clustering_dbscan_eps .value )
392+ min_samples = 5
393+ if self .clustering_dbscan_min_samples .value is not None :
394+ min_samples = int (self .clustering_dbscan_min_samples .value )
336395 return DBSCAN (eps = eps , min_samples = min_samples )
337- elif self .clustering_type == CLUSTERING_AGGLOMERATIVE :
338- n_clusters = int (self .clustering_agglomerative_n_clusters .value )
396+ elif clustering_type == CLUSTERING_AGGLOMERATIVE :
397+ n_clusters = 2
398+ if self .clustering_agglomerative_n_clusters .value is not None :
399+ n_clusters = int (self .clustering_agglomerative_n_clusters .value )
339400 return AgglomerativeClustering (n_clusters = n_clusters )
340401
341- async def update_graph_handler (self , _ = None ):
342- await run .io_bound (self .update_graph )
402+ async def update_csv_handler (self , file ):
403+ await run .io_bound (self .update_csv , file )
404+ await self .update_dataset_handler ()
343405
344406 async def update_dataset_handler (self , _ = None ):
345407 await run .io_bound (self .update_dataset )
408+ await self .update_graph_handler ()
409+
410+ async def update_graph_handler (self , _ = None ):
411+ await run .io_bound (self .update_graph )
412+ await self .update_plot_handler ()
413+
414+ async def update_plot_handler (self , _ = None ):
415+ await run .io_bound (self .update_plot )
416+
417+ def update_csv (self , file ):
418+ if file is None :
419+ logger .warning ("No file uploaded" )
420+ return
421+ self .csv_file = file
346422
347423 def update_dataset (self , _ = None ):
348- self .X , self .labels = self .render_dataset ()
349- self .update_graph ()
424+ self .df_X , self .labels = self .render_dataset ()
350425
351426 def update_graph (self , _ = None ):
352427 self .lens = self .render_lens ()
353428 if self .lens is None :
429+ logger .warning ("No lens selected" )
354430 return
355- if self .X is None :
431+ if self .df_X is None or self .df_X .empty :
432+ logger .warning ("No dataset loaded for computation" )
356433 return
434+ logger .info (f"Uploaded dataset with shape { self .df_X .shape } " )
435+ self .X = self .df_X .to_numpy ()
357436 self .y = self .lens (self .X )
358437 cover = self .render_cover ()
359438 if cover is None :
439+ logger .warning ("No cover selected" )
360440 return
361441 clustering = self .render_clustering ()
362442 if clustering is None :
443+ logger .warning ("No clustering selected" )
363444 return
364445 mapper_algo = MapperAlgorithm (
365446 cover = cover ,
366447 clustering = clustering ,
367448 verbose = False ,
368449 )
450+ logger .info (f"Configuration: { mapper_algo } " )
369451 self .mapper_graph = mapper_algo .fit_transform (self .X , self .y )
370- self .update_plot ()
371-
372- async def update_plot_handler (self , _ = None ):
373- await run .io_bound (self .update_plot )
374452
375453 def update_plot (self ):
454+ if self .df_X is None or self .df_X .empty :
455+ logger .warning ("No dataset loaded for plotting" )
456+ return
376457 if self .mapper_graph is None :
458+ logger .warning ("No graph computed" )
377459 return
378460
379461 dim = 3
@@ -389,13 +471,23 @@ def update_plot(self):
389471 iterations = iterations ,
390472 seed = 42 ,
391473 )
392- colors = pd .concat ([self .labels , self .X ], axis = 1 )
474+
475+ colors = pd .concat ([self .labels , self .df_X ], axis = 1 )
393476 colors_arr = colors .to_numpy ()
394477 color_names = colors .columns .tolist ()
478+
479+ agg = np .nanmean
480+ if self .draw_aggregation .value == DRAW_MEAN :
481+ agg = np .nanmean
482+ elif self .draw_aggregation .value == DRAW_MEDIAN :
483+ agg = np .nanmedian
484+ elif self .draw_aggregation .value == DRAW_MODE :
485+ agg = mode
486+
395487 mapper_fig = mapper_plot .plot_plotly (
396488 colors = colors_arr ,
397489 cmap = ["jet" , "viridis" , "cividis" ],
398- agg = mode ,
490+ agg = agg ,
399491 title = color_names ,
400492 width = 800 ,
401493 height = 800 ,
@@ -408,8 +500,10 @@ def update_plot(self):
408500 ui .plotly (mapper_fig )
409501
410502 def __init__ (self ):
503+ self .csv_file = None
504+ self .df_X = None
411505 with ui .row ().classes ("w-full h-screen m-0 p-0 gap-0 overflow-hidden" ):
412- with ui .column ().classes ("w-64 h-full m-0 p-0" ): # fixed-width sidebar
506+ with ui .column ().classes ("w-64 h-full m-0 p-0" ):
413507 with ui .column ().classes ("w-64 h-full overflow-y-auto p-3 gap-2" ):
414508 with ui .card ().classes ("w-full" ):
415509 ui .markdown ("#### 📊 Data" )
@@ -429,7 +523,14 @@ def __init__(self):
429523 self .build_draw ()
430524 self .build_plot ()
431525 self .update_dataset ()
526+ self .update_graph ()
527+ self .update_plot ()
528+
529+
530+ def main ():
531+ App ()
532+ ui .run ()
432533
433534
434- app = App ()
435- ui . run ()
535+ if __name__ in { "__main__" , "__mp_main__" , "tdamapper.app" }:
536+ main ()
0 commit comments