apmtools 0.1.63__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {apmtools-0.1.63 → apmtools-0.2.1}/PKG-INFO +5 -1
- {apmtools-0.1.63 → apmtools-0.2.1}/pyproject.toml +8 -1
- {apmtools-0.1.63 → apmtools-0.2.1}/src/apmtools/classes.py +68 -0
- apmtools-0.2.1/src/apmtools/shiny/__init__.py +5 -0
- apmtools-0.2.1/src/apmtools/shiny/summary.py +353 -0
- {apmtools-0.1.63 → apmtools-0.2.1}/.gitignore +0 -0
- {apmtools-0.1.63 → apmtools-0.2.1}/LICENSE +0 -0
- {apmtools-0.1.63 → apmtools-0.2.1}/README.md +0 -0
- {apmtools-0.1.63 → apmtools-0.2.1}/src/apmtools/__init__.py +0 -0
- {apmtools-0.1.63 → apmtools-0.2.1}/src/apmtools/data_processing.py +0 -0
- {apmtools-0.1.63 → apmtools-0.2.1}/src/apmtools/functions.py +0 -0
- {apmtools-0.1.63 → apmtools-0.2.1}/src/apmtools/legacy.py +0 -0
- {apmtools-0.1.63 → apmtools-0.2.1}/src/apmtools/units.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: apmtools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.001
|
|
4
4
|
Summary: A collection of tools for processing air pollution monitoring data
|
|
5
5
|
Project-URL: Homepage, https://github.com/federlorenz/apmtools
|
|
6
6
|
Author-email: Federico Lorenzetti <lorenzetti.federico@gmail.com>
|
|
@@ -9,6 +9,10 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
9
9
|
Classifier: Operating System :: OS Independent
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Requires-Python: >=3.8
|
|
12
|
+
Provides-Extra: shiny
|
|
13
|
+
Requires-Dist: plotly>=6.0; extra == 'shiny'
|
|
14
|
+
Requires-Dist: shiny>=1.10; extra == 'shiny'
|
|
15
|
+
Requires-Dist: shinywidgets>=0.6; extra == 'shiny'
|
|
12
16
|
Description-Content-Type: text/markdown
|
|
13
17
|
|
|
14
18
|
# apmtools
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "apmtools"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.001"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Federico Lorenzetti", email="lorenzetti.federico@gmail.com" },
|
|
10
10
|
]
|
|
@@ -19,3 +19,10 @@ classifiers = [
|
|
|
19
19
|
|
|
20
20
|
[project.urls]
|
|
21
21
|
Homepage = "https://github.com/federlorenz/apmtools"
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
shiny = [
|
|
25
|
+
"shiny>=1.10",
|
|
26
|
+
"shinywidgets>=0.6",
|
|
27
|
+
"plotly>=6.0",
|
|
28
|
+
]
|
|
@@ -1397,6 +1397,74 @@ class Summary(pd.DataFrame):
|
|
|
1397
1397
|
|
|
1398
1398
|
return output
|
|
1399
1399
|
|
|
1400
|
+
def plot_app(
|
|
1401
|
+
self,
|
|
1402
|
+
x=None,
|
|
1403
|
+
y=None,
|
|
1404
|
+
group_by=None,
|
|
1405
|
+
plot_type="line",
|
|
1406
|
+
title="Summary plot",
|
|
1407
|
+
host="127.0.0.1",
|
|
1408
|
+
port=0,
|
|
1409
|
+
launch_browser=True,
|
|
1410
|
+
daemon=True,
|
|
1411
|
+
):
|
|
1412
|
+
"""
|
|
1413
|
+
Launch an interactive Shiny plotting application for this Summary.
|
|
1414
|
+
|
|
1415
|
+
Parameters
|
|
1416
|
+
----------
|
|
1417
|
+
x : str, optional
|
|
1418
|
+
Initial X-axis or categorical column.
|
|
1419
|
+
y : str or sequence of str, optional
|
|
1420
|
+
Initial numeric variable or variables.
|
|
1421
|
+
group_by : str, optional
|
|
1422
|
+
Initial grouping column.
|
|
1423
|
+
plot_type : {"line", "bar", "histogram", "box"}, default="line"
|
|
1424
|
+
Initial plot type. The user can change the plot type in the app.
|
|
1425
|
+
title : str, default="Summary plot"
|
|
1426
|
+
Initial plot title.
|
|
1427
|
+
host : str, default="127.0.0.1"
|
|
1428
|
+
Server host.
|
|
1429
|
+
port : int, default=0
|
|
1430
|
+
Server port. Zero selects an available port.
|
|
1431
|
+
launch_browser : bool, default=True
|
|
1432
|
+
Open the app in the default browser.
|
|
1433
|
+
daemon : bool, default=True
|
|
1434
|
+
Run the server thread as a daemon.
|
|
1435
|
+
|
|
1436
|
+
Returns
|
|
1437
|
+
-------
|
|
1438
|
+
threading.Thread
|
|
1439
|
+
Background thread running the Shiny server.
|
|
1440
|
+
|
|
1441
|
+
Notes
|
|
1442
|
+
-----
|
|
1443
|
+
Install the optional dependencies with:
|
|
1444
|
+
|
|
1445
|
+
pip install "apmtools[shiny]"
|
|
1446
|
+
"""
|
|
1447
|
+
try:
|
|
1448
|
+
from .shiny import run_summary_app
|
|
1449
|
+
except ImportError as exc:
|
|
1450
|
+
raise ImportError(
|
|
1451
|
+
"Summary.plot_app() requires the optional Shiny dependencies. "
|
|
1452
|
+
'Install them with: pip install "apmtools[shiny]"'
|
|
1453
|
+
) from exc
|
|
1454
|
+
|
|
1455
|
+
return run_summary_app(
|
|
1456
|
+
self,
|
|
1457
|
+
x=x,
|
|
1458
|
+
y=y,
|
|
1459
|
+
group_by=group_by,
|
|
1460
|
+
plot_type=plot_type,
|
|
1461
|
+
title=title,
|
|
1462
|
+
host=host,
|
|
1463
|
+
port=port,
|
|
1464
|
+
launch_browser=launch_browser,
|
|
1465
|
+
daemon=daemon,
|
|
1466
|
+
)
|
|
1467
|
+
|
|
1400
1468
|
class Apm(pd.DataFrame):
|
|
1401
1469
|
monitor = "apm"
|
|
1402
1470
|
def __init__(self, *args, **kwargs):
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Interactive Plotly/Shiny application for apmtools Summary objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
PLOT_TYPES = {
|
|
12
|
+
"line": "Line",
|
|
13
|
+
"bar": "Bar",
|
|
14
|
+
"histogram": "Histogram",
|
|
15
|
+
"box": "Boxplot",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _default_x(data: pd.DataFrame) -> str:
|
|
20
|
+
"""Return the first datetime column, or otherwise the first column."""
|
|
21
|
+
datetime_columns = [
|
|
22
|
+
column
|
|
23
|
+
for column in data.columns
|
|
24
|
+
if pd.api.types.is_datetime64_any_dtype(data[column])
|
|
25
|
+
]
|
|
26
|
+
return str(datetime_columns[0] if datetime_columns else data.columns[0])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _numeric_columns(data: pd.DataFrame) -> list[str]:
|
|
30
|
+
"""Return numeric column names as strings."""
|
|
31
|
+
return [
|
|
32
|
+
str(column)
|
|
33
|
+
for column in data.columns
|
|
34
|
+
if pd.api.types.is_numeric_dtype(data[column])
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _normalise_y(
|
|
39
|
+
data: pd.DataFrame,
|
|
40
|
+
x: str,
|
|
41
|
+
y: str | Sequence[str] | None,
|
|
42
|
+
) -> list[str]:
|
|
43
|
+
"""Validate and normalise an initial Y selection."""
|
|
44
|
+
if y is None:
|
|
45
|
+
return [column for column in _numeric_columns(data) if column != x][:1]
|
|
46
|
+
|
|
47
|
+
values = [y] if isinstance(y, str) else list(y)
|
|
48
|
+
missing = [column for column in values if column not in data.columns]
|
|
49
|
+
if missing:
|
|
50
|
+
raise ValueError(f"Unknown y column(s): {missing}")
|
|
51
|
+
return [str(column) for column in values]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def create_summary_app(
|
|
55
|
+
data: pd.DataFrame,
|
|
56
|
+
*,
|
|
57
|
+
x: str | None = None,
|
|
58
|
+
y: str | Sequence[str] | None = None,
|
|
59
|
+
group_by: str | None = None,
|
|
60
|
+
plot_type: str = "line",
|
|
61
|
+
title: str = "Summary plot",
|
|
62
|
+
):
|
|
63
|
+
"""
|
|
64
|
+
Create, but do not start, a Shiny application for a Summary/DataFrame.
|
|
65
|
+
|
|
66
|
+
Supported plot types are ``line``, ``bar``, ``histogram`` and ``box``.
|
|
67
|
+
"""
|
|
68
|
+
from shiny import App, reactive, render, ui
|
|
69
|
+
from shinywidgets import output_widget, render_plotly
|
|
70
|
+
import plotly.express as px
|
|
71
|
+
|
|
72
|
+
df = pd.DataFrame(data).copy()
|
|
73
|
+
if df.empty:
|
|
74
|
+
raise ValueError("Cannot create a Shiny application from an empty Summary.")
|
|
75
|
+
|
|
76
|
+
original_columns = list(df.columns)
|
|
77
|
+
column_map = {str(column): column for column in original_columns}
|
|
78
|
+
columns = list(column_map)
|
|
79
|
+
numeric_columns = _numeric_columns(df)
|
|
80
|
+
|
|
81
|
+
if x is None:
|
|
82
|
+
x = _default_x(df)
|
|
83
|
+
else:
|
|
84
|
+
x = str(x)
|
|
85
|
+
if x not in column_map:
|
|
86
|
+
raise ValueError(f"Unknown x column: {x!r}")
|
|
87
|
+
|
|
88
|
+
initial_y = _normalise_y(df, column_map[x], y)
|
|
89
|
+
|
|
90
|
+
if group_by is not None:
|
|
91
|
+
group_by = str(group_by)
|
|
92
|
+
if group_by not in column_map:
|
|
93
|
+
raise ValueError(f"Unknown group_by column: {group_by!r}")
|
|
94
|
+
|
|
95
|
+
if plot_type not in PLOT_TYPES:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"Unknown plot_type {plot_type!r}. "
|
|
98
|
+
f"Choose one of: {', '.join(PLOT_TYPES)}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
app_ui = ui.page_sidebar(
|
|
102
|
+
ui.sidebar(
|
|
103
|
+
ui.h4("Plot controls"),
|
|
104
|
+
|
|
105
|
+
ui.input_select(
|
|
106
|
+
"plot_type",
|
|
107
|
+
"Plot type",
|
|
108
|
+
choices=PLOT_TYPES,
|
|
109
|
+
selected=plot_type,
|
|
110
|
+
),
|
|
111
|
+
|
|
112
|
+
ui.input_select(
|
|
113
|
+
"x",
|
|
114
|
+
"X-axis / category",
|
|
115
|
+
choices=columns,
|
|
116
|
+
selected=x,
|
|
117
|
+
),
|
|
118
|
+
|
|
119
|
+
ui.input_selectize(
|
|
120
|
+
"y",
|
|
121
|
+
"Y-axis / variable",
|
|
122
|
+
choices=numeric_columns,
|
|
123
|
+
selected=initial_y,
|
|
124
|
+
multiple=True,
|
|
125
|
+
),
|
|
126
|
+
|
|
127
|
+
ui.input_select(
|
|
128
|
+
"group_by",
|
|
129
|
+
"Group by",
|
|
130
|
+
choices={"None": "None", **{c: c for c in columns}},
|
|
131
|
+
selected=group_by if group_by is not None else "None",
|
|
132
|
+
),
|
|
133
|
+
|
|
134
|
+
ui.input_numeric(
|
|
135
|
+
"bins",
|
|
136
|
+
"Histogram bins",
|
|
137
|
+
value=30,
|
|
138
|
+
min=1,
|
|
139
|
+
max=200,
|
|
140
|
+
step=1,
|
|
141
|
+
),
|
|
142
|
+
|
|
143
|
+
ui.input_text(
|
|
144
|
+
"title",
|
|
145
|
+
"Plot title",
|
|
146
|
+
value=title,
|
|
147
|
+
),
|
|
148
|
+
|
|
149
|
+
ui.help_text(
|
|
150
|
+
"Line: ordered X values and one or more Y variables. "
|
|
151
|
+
"Bar: categorical X and numeric Y. "
|
|
152
|
+
"Histogram: distribution of numeric variables. "
|
|
153
|
+
"Boxplot: distribution of numeric variables, optionally grouped."
|
|
154
|
+
),
|
|
155
|
+
),
|
|
156
|
+
|
|
157
|
+
ui.card(
|
|
158
|
+
ui.card_header("Interactive plot"),
|
|
159
|
+
output_widget("plot", height="75vh"),
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def server(input, output, session):
|
|
164
|
+
@reactive.calc
|
|
165
|
+
def selected_data() -> pd.DataFrame:
|
|
166
|
+
selected_plot = input.plot_type()
|
|
167
|
+
selected_x = input.x()
|
|
168
|
+
selected_y = list(input.y())
|
|
169
|
+
selected_group = input.group_by()
|
|
170
|
+
|
|
171
|
+
if selected_plot == "histogram":
|
|
172
|
+
if not selected_y:
|
|
173
|
+
return pd.DataFrame()
|
|
174
|
+
use_columns = [column_map[c] for c in selected_y]
|
|
175
|
+
else:
|
|
176
|
+
if not selected_y:
|
|
177
|
+
return pd.DataFrame()
|
|
178
|
+
use_columns = [
|
|
179
|
+
column_map[selected_x],
|
|
180
|
+
*(column_map[c] for c in selected_y),
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
if selected_group != "None":
|
|
184
|
+
group_column = column_map[selected_group]
|
|
185
|
+
if group_column not in use_columns:
|
|
186
|
+
use_columns.append(group_column)
|
|
187
|
+
|
|
188
|
+
return df.loc[:, use_columns].copy()
|
|
189
|
+
|
|
190
|
+
@render_plotly
|
|
191
|
+
def plot():
|
|
192
|
+
plot_df = selected_data()
|
|
193
|
+
selected_plot = input.plot_type()
|
|
194
|
+
selected_x = input.x()
|
|
195
|
+
selected_y = list(input.y())
|
|
196
|
+
selected_group = input.group_by()
|
|
197
|
+
|
|
198
|
+
if plot_df.empty or not selected_y:
|
|
199
|
+
return px.scatter(title="Select at least one numeric variable")
|
|
200
|
+
|
|
201
|
+
y_columns = [column_map[c] for c in selected_y]
|
|
202
|
+
|
|
203
|
+
if selected_plot == "histogram":
|
|
204
|
+
# Plotly accepts multiple columns through y with a long-form
|
|
205
|
+
# transformation; melt gives consistent legends.
|
|
206
|
+
long_df = plot_df[y_columns].melt(
|
|
207
|
+
var_name="variable",
|
|
208
|
+
value_name="value",
|
|
209
|
+
)
|
|
210
|
+
fig = px.histogram(
|
|
211
|
+
long_df,
|
|
212
|
+
x="value",
|
|
213
|
+
color="variable",
|
|
214
|
+
nbins=int(input.bins()),
|
|
215
|
+
barmode="overlay" if len(y_columns) > 1 else "relative",
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
elif selected_plot == "box":
|
|
219
|
+
if selected_group == "None":
|
|
220
|
+
long_df = plot_df[y_columns].melt(
|
|
221
|
+
var_name="variable",
|
|
222
|
+
value_name="value",
|
|
223
|
+
)
|
|
224
|
+
fig = px.box(
|
|
225
|
+
long_df,
|
|
226
|
+
x="variable",
|
|
227
|
+
y="value",
|
|
228
|
+
points="outliers",
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
group_column = column_map[selected_group]
|
|
232
|
+
long_df = plot_df[
|
|
233
|
+
[group_column, *y_columns]
|
|
234
|
+
].melt(
|
|
235
|
+
id_vars=[group_column],
|
|
236
|
+
var_name="variable",
|
|
237
|
+
value_name="value",
|
|
238
|
+
)
|
|
239
|
+
fig = px.box(
|
|
240
|
+
long_df,
|
|
241
|
+
x="variable",
|
|
242
|
+
y="value",
|
|
243
|
+
color=group_column,
|
|
244
|
+
points="outliers",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
elif selected_plot == "bar":
|
|
248
|
+
x_column = column_map[selected_x]
|
|
249
|
+
if selected_group == "None":
|
|
250
|
+
fig = px.bar(
|
|
251
|
+
plot_df,
|
|
252
|
+
x=x_column,
|
|
253
|
+
y=y_columns,
|
|
254
|
+
barmode="group",
|
|
255
|
+
)
|
|
256
|
+
else:
|
|
257
|
+
group_column = column_map[selected_group]
|
|
258
|
+
long_df = plot_df.melt(
|
|
259
|
+
id_vars=[x_column, group_column],
|
|
260
|
+
value_vars=y_columns,
|
|
261
|
+
var_name="variable",
|
|
262
|
+
value_name="value",
|
|
263
|
+
)
|
|
264
|
+
fig = px.bar(
|
|
265
|
+
long_df,
|
|
266
|
+
x=x_column,
|
|
267
|
+
y="value",
|
|
268
|
+
color=group_column,
|
|
269
|
+
facet_row="variable" if len(y_columns) > 1 else None,
|
|
270
|
+
barmode="group",
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
else: # line
|
|
274
|
+
x_column = column_map[selected_x]
|
|
275
|
+
if selected_group == "None":
|
|
276
|
+
fig = px.line(
|
|
277
|
+
plot_df,
|
|
278
|
+
x=x_column,
|
|
279
|
+
y=y_columns,
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
group_column = column_map[selected_group]
|
|
283
|
+
long_df = plot_df.melt(
|
|
284
|
+
id_vars=[x_column, group_column],
|
|
285
|
+
value_vars=y_columns,
|
|
286
|
+
var_name="variable",
|
|
287
|
+
value_name="value",
|
|
288
|
+
)
|
|
289
|
+
fig = px.line(
|
|
290
|
+
long_df,
|
|
291
|
+
x=x_column,
|
|
292
|
+
y="value",
|
|
293
|
+
color=group_column,
|
|
294
|
+
facet_row="variable" if len(y_columns) > 1 else None,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
fig.update_layout(
|
|
298
|
+
title=input.title(),
|
|
299
|
+
hovermode="x unified"
|
|
300
|
+
if selected_plot == "line"
|
|
301
|
+
else "closest",
|
|
302
|
+
margin=dict(l=60, r=30, t=70, b=60),
|
|
303
|
+
)
|
|
304
|
+
return fig
|
|
305
|
+
|
|
306
|
+
return App(app_ui, server)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def run_summary_app(
|
|
310
|
+
data: pd.DataFrame,
|
|
311
|
+
*,
|
|
312
|
+
x: str | None = None,
|
|
313
|
+
y: str | Sequence[str] | None = None,
|
|
314
|
+
group_by: str | None = None,
|
|
315
|
+
plot_type: str = "line",
|
|
316
|
+
title: str = "Summary plot",
|
|
317
|
+
host: str = "127.0.0.1",
|
|
318
|
+
port: int = 0,
|
|
319
|
+
launch_browser: bool = True,
|
|
320
|
+
daemon: bool = True,
|
|
321
|
+
):
|
|
322
|
+
"""
|
|
323
|
+
Create and start the Shiny app in a background thread.
|
|
324
|
+
|
|
325
|
+
``port=0`` asks the operating system to select an available port.
|
|
326
|
+
"""
|
|
327
|
+
from shiny import run_app
|
|
328
|
+
|
|
329
|
+
app = create_summary_app(
|
|
330
|
+
data,
|
|
331
|
+
x=x,
|
|
332
|
+
y=y,
|
|
333
|
+
group_by=group_by,
|
|
334
|
+
plot_type=plot_type,
|
|
335
|
+
title=title,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def _run() -> None:
|
|
339
|
+
run_app(
|
|
340
|
+
app,
|
|
341
|
+
host=host,
|
|
342
|
+
port=port,
|
|
343
|
+
launch_browser=launch_browser,
|
|
344
|
+
dev_mode=False,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
thread = threading.Thread(
|
|
348
|
+
target=_run,
|
|
349
|
+
name="apmtools-shiny-summary",
|
|
350
|
+
daemon=daemon,
|
|
351
|
+
)
|
|
352
|
+
thread.start()
|
|
353
|
+
return thread
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|