Skip to content

Commit 665e3c5

Browse files
committed
Added app to src directory. Improved options with csv upload
1 parent 28a58bd commit 665e3c5

2 files changed

Lines changed: 146 additions & 36 deletions

File tree

pyproject.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ dev = [
5252
"flake8",
5353
"nbformat>=4.2.0",
5454
]
55+
app = [
56+
"pandas<3.0.0",
57+
"scikit-learn<1.6.0",
58+
"nicegui>=2.18.0,<3.0.0",
59+
"umap-learn<0.6.0",
60+
]
61+
62+
[project.scripts]
63+
tda-mapper-app = "tdamapper.app:main"
5564

5665
[project.urls]
5766
Homepage = "https://github.com/lucasimi/tda-mapper-python"
Lines changed: 137 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import logging
2+
13
import numpy as np
24
import pandas as pd
35
import plotly.graph_objs as go
46
from nicegui import run, ui
57
from 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
79
from sklearn.decomposition import PCA
810
from umap import UMAP
911

@@ -12,6 +14,9 @@
1214
from tdamapper.learn import MapperAlgorithm
1315
from tdamapper.plot import MapperPlot
1416

17+
logger = logging.getLogger(__name__)
18+
logging.basicConfig(level=logging.INFO)
19+
1520

1621
def 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+
2336
def _identity(X):
2437
return X
2538

@@ -66,6 +79,9 @@ def _func(X):
6679
DRAW_3D = "3D"
6780
DRAW_2D = "2D"
6881
DRAW_ITERATIONS = 50
82+
DRAW_MEAN = "Mean"
83+
DRAW_MEDIAN = "Median"
84+
DRAW_MODE = "Mode"
6985

7086

7187
class 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

Comments
 (0)