Spaces:
Running
Running
maartenbreddels
commited on
Commit
β’
0449aa7
0
Parent(s):
initial commit
Browse files- Dockerfile +11 -0
- README.md +33 -0
- pages/00-readme.py +20 -0
- pages/01-original-with-widgets.ipynb +127 -0
- pages/02-fancy-with-solara.py +120 -0
- requirements.txt +6 -0
Dockerfile
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.9
|
2 |
+
|
3 |
+
WORKDIR /code
|
4 |
+
|
5 |
+
COPY ./requirements.txt /code/requirements.txt
|
6 |
+
|
7 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
8 |
+
|
9 |
+
COPY . .
|
10 |
+
|
11 |
+
CMD ["solara", "run", "pages", "--host=0.0.0.0", "--port", "7860"]
|
README.md
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: Drawdata Sklearn
|
3 |
+
emoji: π
|
4 |
+
colorFrom: indigo
|
5 |
+
colorTo: gray
|
6 |
+
sdk: docker
|
7 |
+
pinned: false
|
8 |
+
license: mit
|
9 |
+
---
|
10 |
+
|
11 |
+
# drawdata - demo using solara
|
12 |
+
|
13 |
+
[Drawdata](https://github.com/koaning/drawdata) is an ipywidget library build using [anywidget](https://github.com/manzt/anywidget).
|
14 |
+
|
15 |
+
All ipywidgets (and also anywidget) run in the Jupyter notebook, but also in [Solara server](https://solara.dev/).
|
16 |
+
|
17 |
+
Solara server can run Python code and notebooks, and display [regular widgets](https://solara.dev/docs/tutorial/ipywidgets),
|
18 |
+
or for more production quality code, [solara components](https://solara.dev/docs/fundamentals/components).
|
19 |
+
|
20 |
+
## Demo 1 - Original with classic widgets
|
21 |
+
|
22 |
+
This demonstrates solara server can execute the ([almost unmodified](https://github.com/widgetti/solara/issues/539)) [notebook](https://github.com/probabl-ai/youtube-appendix/blob/main/04-drawing-data/notebook.ipynb).
|
23 |
+
|
24 |
+
Since ipywidgets lacks lifetime management, the widgets in this tab are kept alive and reused when moved back to this tab (try drawing, and switch tabs).
|
25 |
+
|
26 |
+
This demo shows how drawdata can be used exploring how different sklearn algorithm perform on datasets draw by hand ([YouTube video](https://www.youtube.com/watch?v=STPv0jSAQEk))
|
27 |
+
|
28 |
+
|
29 |
+
## Demo 2 - Fancy demo using components
|
30 |
+
|
31 |
+
For larger apps and dynamic UI's it pays off using [components](https://solara.dev/docs/fundamentals/components) and creating elements
|
32 |
+
instead of directly creating widgets. This makes it possible to create very dynamic UIs (try changing the layout using the top right button in the app bar)
|
33 |
+
and have proper life cycle management (widgets get cleaned up when changing tabs, freeing memory - very important in larger applications)
|
pages/00-readme.py
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import solara
|
2 |
+
from pathlib import Path
|
3 |
+
|
4 |
+
|
5 |
+
HERE = Path(__file__).parent
|
6 |
+
|
7 |
+
|
8 |
+
@solara.component
|
9 |
+
def Page():
|
10 |
+
md_text = (HERE.parent / "README.md").read_text()
|
11 |
+
# take out front matter
|
12 |
+
md_text = md_text.rpartition("---")[2]
|
13 |
+
solara.Markdown(md_text)
|
14 |
+
solara.Title("Draw data widget demo with Solara βοΈ")
|
15 |
+
|
16 |
+
|
17 |
+
|
18 |
+
@solara.component
|
19 |
+
def Layout(children):
|
20 |
+
solara.AppLayout(children=children)
|
pages/01-original-with-widgets.ipynb
ADDED
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"cells": [
|
3 |
+
{
|
4 |
+
"cell_type": "code",
|
5 |
+
"execution_count": 7,
|
6 |
+
"id": "eea9f1b8-4240-4a68-a412-2b4071b2c04a",
|
7 |
+
"metadata": {},
|
8 |
+
"outputs": [],
|
9 |
+
"source": [
|
10 |
+
"from drawdata import ScatterWidget"
|
11 |
+
]
|
12 |
+
},
|
13 |
+
{
|
14 |
+
"cell_type": "code",
|
15 |
+
"execution_count": 8,
|
16 |
+
"id": "207dbcfd-e731-4758-8035-d1f429aa10d4",
|
17 |
+
"metadata": {},
|
18 |
+
"outputs": [],
|
19 |
+
"source": [
|
20 |
+
"widget = ScatterWidget()"
|
21 |
+
]
|
22 |
+
},
|
23 |
+
{
|
24 |
+
"cell_type": "code",
|
25 |
+
"execution_count": 13,
|
26 |
+
"id": "b77030f4-c895-4a39-96ce-b79d6a8a6d69",
|
27 |
+
"metadata": {},
|
28 |
+
"outputs": [],
|
29 |
+
"source": [
|
30 |
+
"import matplotlib.pyplot as plt\n",
|
31 |
+
"from IPython.core.display import HTML\n",
|
32 |
+
"from sklearn.linear_model import LogisticRegression\n",
|
33 |
+
"from sklearn.inspection import DecisionBoundaryDisplay\n",
|
34 |
+
"from sklearn.tree import DecisionTreeClassifier\n",
|
35 |
+
"\n",
|
36 |
+
"import matplotlib.pylab as plt \n",
|
37 |
+
"import numpy as np\n",
|
38 |
+
"import ipywidgets"
|
39 |
+
]
|
40 |
+
},
|
41 |
+
{
|
42 |
+
"cell_type": "code",
|
43 |
+
"execution_count": null,
|
44 |
+
"id": "b9d36c79-3d1d-4084-a1a6-43f3b95c06fe",
|
45 |
+
"metadata": {},
|
46 |
+
"outputs": [
|
47 |
+
{
|
48 |
+
"data": {
|
49 |
+
"application/vnd.jupyter.widget-view+json": {
|
50 |
+
"model_id": "6b21ddf1e80a4a34a786547610d52aa0",
|
51 |
+
"version_major": 2,
|
52 |
+
"version_minor": 0
|
53 |
+
},
|
54 |
+
"text/plain": [
|
55 |
+
"HBox(children=(ScatterWidget(), Output()))"
|
56 |
+
]
|
57 |
+
},
|
58 |
+
"execution_count": 17,
|
59 |
+
"metadata": {},
|
60 |
+
"output_type": "execute_result"
|
61 |
+
}
|
62 |
+
],
|
63 |
+
"source": [
|
64 |
+
"widget = ScatterWidget()\n",
|
65 |
+
"output = ipywidgets.Output()\n",
|
66 |
+
"\n",
|
67 |
+
"\n",
|
68 |
+
"@output.capture(clear_output=True)\n",
|
69 |
+
"def on_change(change):\n",
|
70 |
+
" df = widget.data_as_pandas\n",
|
71 |
+
" if len(df) and (df['color'].nunique() > 1):\n",
|
72 |
+
" X = df[['x', 'y']].values\n",
|
73 |
+
" y = df['color']\n",
|
74 |
+
" display(HTML(\"<br><br><br>\"))\n",
|
75 |
+
" fig = plt.figure(figsize=(12, 12));\n",
|
76 |
+
" classifier = DecisionTreeClassifier().fit(X, y)\n",
|
77 |
+
" disp = DecisionBoundaryDisplay.from_estimator(\n",
|
78 |
+
" classifier, X, \n",
|
79 |
+
" ax=fig.add_subplot(111),\n",
|
80 |
+
" response_method=\"predict_proba\" if len(np.unique(df['color'])) == 2 else \"predict\",\n",
|
81 |
+
" xlabel=\"x\", ylabel=\"y\",\n",
|
82 |
+
" alpha=0.5,\n",
|
83 |
+
" );\n",
|
84 |
+
" disp.ax_.scatter(X[:, 0], X[:, 1], c=y, edgecolor=\"k\");\n",
|
85 |
+
" # plt.title(f\"{classifier.__class__.__name__}\");\n",
|
86 |
+
" disp.ax_.set_title(f\"{classifier.__class__.__name__}\");\n",
|
87 |
+
" # plt.show();\n",
|
88 |
+
" import solara\n",
|
89 |
+
" solara.display(solara.FigureMatplotlib(fig))\n",
|
90 |
+
"widget.observe(on_change, names=[\"data\"])\n",
|
91 |
+
"on_change(None)\n",
|
92 |
+
"page = ipywidgets.HBox([widget, output])\n",
|
93 |
+
"page"
|
94 |
+
]
|
95 |
+
},
|
96 |
+
{
|
97 |
+
"cell_type": "markdown",
|
98 |
+
"id": "5f9bb7ce-8a7f-4879-9ddf-5b256bb0ff64",
|
99 |
+
"metadata": {},
|
100 |
+
"source": [
|
101 |
+
"\n",
|
102 |
+
"p<br><br><br><br><br><br><br><br><br><br><br><br>"
|
103 |
+
]
|
104 |
+
}
|
105 |
+
],
|
106 |
+
"metadata": {
|
107 |
+
"kernelspec": {
|
108 |
+
"display_name": "Python 3 (ipykernel)",
|
109 |
+
"language": "python",
|
110 |
+
"name": "python3"
|
111 |
+
},
|
112 |
+
"language_info": {
|
113 |
+
"codemirror_mode": {
|
114 |
+
"name": "ipython",
|
115 |
+
"version": 3
|
116 |
+
},
|
117 |
+
"file_extension": ".py",
|
118 |
+
"mimetype": "text/x-python",
|
119 |
+
"name": "python",
|
120 |
+
"nbconvert_exporter": "python",
|
121 |
+
"pygments_lexer": "ipython3",
|
122 |
+
"version": "3.11.6"
|
123 |
+
}
|
124 |
+
},
|
125 |
+
"nbformat": 4,
|
126 |
+
"nbformat_minor": 5
|
127 |
+
}
|
pages/02-fancy-with-solara.py
ADDED
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List
|
2 |
+
from traitlets import Dict
|
3 |
+
import solara
|
4 |
+
import solara.lab
|
5 |
+
import matplotlib.pyplot as plt
|
6 |
+
|
7 |
+
# needed for solara up to version 1.28
|
8 |
+
plt.switch_backend("module://matplotlib_inline.backend_inline")
|
9 |
+
# title = "solara"
|
10 |
+
|
11 |
+
from sklearn.linear_model import LogisticRegression
|
12 |
+
from sklearn.inspection import DecisionBoundaryDisplay
|
13 |
+
from sklearn.tree import DecisionTreeClassifier
|
14 |
+
|
15 |
+
import matplotlib.pylab as plt
|
16 |
+
import numpy as np
|
17 |
+
import pandas as pd
|
18 |
+
|
19 |
+
from drawdata import ScatterWidget
|
20 |
+
|
21 |
+
drawdata: solara.Reactive[List[Dict]] = solara.reactive([])
|
22 |
+
|
23 |
+
|
24 |
+
@solara.component
|
25 |
+
def ClassifierDraw(classifier, X, y, response_method="predict_proba", figsize=(8, 8)):
|
26 |
+
fig = plt.figure(figsize=figsize)
|
27 |
+
disp = DecisionBoundaryDisplay.from_estimator(
|
28 |
+
classifier,
|
29 |
+
X,
|
30 |
+
# not sure why this was needed, otherwise i get a blank plot
|
31 |
+
ax=fig.add_subplot(111),
|
32 |
+
response_method=response_method,
|
33 |
+
xlabel="x",
|
34 |
+
ylabel="y",
|
35 |
+
alpha=0.5,
|
36 |
+
)
|
37 |
+
disp.ax_.scatter(X[:, 0], X[:, 1], c=y, edgecolor="k")
|
38 |
+
plt.title(f"{classifier.__class__.__name__}")
|
39 |
+
plt.close()
|
40 |
+
solara.FigureMatplotlib(fig)
|
41 |
+
|
42 |
+
|
43 |
+
@solara.component
|
44 |
+
def DecisionTreeClassifierDraw(df):
|
45 |
+
criterion = solara.use_reactive("gini")
|
46 |
+
splitter = solara.use_reactive("best")
|
47 |
+
with solara.Row():
|
48 |
+
solara.ToggleButtonsSingle(value=criterion, values=["gini", "entropy", "log_loss"])
|
49 |
+
solara.ToggleButtonsSingle(value=splitter, values=["best", "random"])
|
50 |
+
X = df[["x", "y"]].values
|
51 |
+
y = df["color"]
|
52 |
+
classifier = DecisionTreeClassifier(criterion=criterion.value, splitter=splitter.value).fit(X, y)
|
53 |
+
ClassifierDraw(classifier, X, y, "predict_proba" if len(np.unique(df["color"])) == 2 else "predict")
|
54 |
+
|
55 |
+
|
56 |
+
@solara.component
|
57 |
+
def LogisticRegressionDraw(df):
|
58 |
+
penalty = solara.use_reactive("l2")
|
59 |
+
solver = solara.use_reactive("lbfgs")
|
60 |
+
l1_ratio = solara.use_reactive(0.5)
|
61 |
+
with solara.Row():
|
62 |
+
solara.ToggleButtonsSingle(value=penalty, values=["l1", "l2", "elasticnet", "none"])
|
63 |
+
solara.ToggleButtonsSingle(value=solver, values=["newton-cg", "lbfgs", "liblinear", "sag", "saga"])
|
64 |
+
if penalty.value == "elasticnet":
|
65 |
+
solara.FloatSlider("l1_ratio", value=l1_ratio, min=0, max=1, step=0.1)
|
66 |
+
X = df[["x", "y"]].values
|
67 |
+
y = df["color"]
|
68 |
+
try:
|
69 |
+
classifier = LogisticRegression(penalty=penalty.value, solver=solver.value, l1_ratio=l1_ratio.value).fit(X, y)
|
70 |
+
except ValueError as e:
|
71 |
+
solara.Error(str(e))
|
72 |
+
else:
|
73 |
+
ClassifierDraw(classifier, X, y, "predict_proba" if len(np.unique(df["color"])) == 2 else "predict")
|
74 |
+
|
75 |
+
|
76 |
+
@solara.component
|
77 |
+
def Page():
|
78 |
+
vertical = solara.use_reactive(True)
|
79 |
+
solara.AppBarTitle("Draw Data with Solara demo")
|
80 |
+
df = pd.DataFrame(drawdata.value) if drawdata.value else None
|
81 |
+
|
82 |
+
with solara.AppBar():
|
83 |
+
# TODO: doesn't work, ScatterWidget does not update when data is updated (read only?)
|
84 |
+
# solara.Button(icon_name="mdi-delete", on_click=lambda: drawdata.set([]), icon=True)
|
85 |
+
# demo how solara can dynamically change the layout
|
86 |
+
solara.Button(icon_name="mdi-align-vertical-top" if vertical.value else "mdi-align-horizontal-left", on_click=lambda: vertical.set(not vertical.value), icon=True)
|
87 |
+
|
88 |
+
# if solara.lab.theme.dark_effective:
|
89 |
+
# plt.style.use('dark_background')
|
90 |
+
# else:
|
91 |
+
# plt.style.use('default')
|
92 |
+
with solara.Column() if vertical.value else solara.Row():
|
93 |
+
# with solara, we don't just create the widget, but an element that describes it
|
94 |
+
# and instead of observe, we have on_<trait> callbacks
|
95 |
+
# Note: if we store the data in the reactive var (drawdata), we keep the drawing
|
96 |
+
# on hot reload.
|
97 |
+
ScatterWidget.element(data=drawdata.value, on_data=drawdata.set)
|
98 |
+
# downside of using elements and components: we cannot call method on the widget
|
99 |
+
# so we need to re-create the dataframe ourselves
|
100 |
+
with solara.lab.Tabs():
|
101 |
+
with solara.lab.Tab("classifier"):
|
102 |
+
if df is not None and (df["color"].nunique() > 1):
|
103 |
+
with solara.Column(style={"max-height": "500px", "padding-top": "0px"}):
|
104 |
+
with solara.lab.Tabs():
|
105 |
+
with solara.lab.Tab("DecisionTreeClassifier"):
|
106 |
+
DecisionTreeClassifierDraw(df)
|
107 |
+
with solara.lab.Tab("LogisticRegressionDraw"):
|
108 |
+
LogisticRegressionDraw(df)
|
109 |
+
else:
|
110 |
+
with solara.Column(style={"justify-content": "center"}) if not vertical.value else solara.Row():
|
111 |
+
solara.Info("Choose at least two colors to draw a decision boundary.")
|
112 |
+
with solara.lab.Tab("table view"):
|
113 |
+
if df is not None:
|
114 |
+
with solara.FileDownload(data=lambda: df.to_csv(), filename="drawdata.csv"):
|
115 |
+
solara.Button("download as csv", icon_name="mdi-download", outlined=True, color="primary")
|
116 |
+
solara.DataFrame(df)
|
117 |
+
|
118 |
+
|
119 |
+
# in the notebook:
|
120 |
+
Page()
|
requirements.txt
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
solara==1.28
|
2 |
+
drawdata==0.3.0
|
3 |
+
matplotlib
|
4 |
+
scikit-learn
|
5 |
+
pandas
|
6 |
+
numpy
|