apmtools 0.2.17__tar.gz → 0.2.19__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.2.17 → apmtools-0.2.19}/PKG-INFO +1 -1
- {apmtools-0.2.17 → apmtools-0.2.19}/pyproject.toml +1 -1
- {apmtools-0.2.17 → apmtools-0.2.19}/src/apmtools/shiny/summary.py +144 -23
- {apmtools-0.2.17 → apmtools-0.2.19}/.gitignore +0 -0
- {apmtools-0.2.17 → apmtools-0.2.19}/LICENSE +0 -0
- {apmtools-0.2.17 → apmtools-0.2.19}/README.md +0 -0
- {apmtools-0.2.17 → apmtools-0.2.19}/src/apmtools/__init__.py +0 -0
- {apmtools-0.2.17 → apmtools-0.2.19}/src/apmtools/classes.py +0 -0
- {apmtools-0.2.17 → apmtools-0.2.19}/src/apmtools/data_processing.py +0 -0
- {apmtools-0.2.17 → apmtools-0.2.19}/src/apmtools/functions.py +0 -0
- {apmtools-0.2.17 → apmtools-0.2.19}/src/apmtools/legacy.py +0 -0
- {apmtools-0.2.17 → apmtools-0.2.19}/src/apmtools/shiny/__init__.py +0 -0
- {apmtools-0.2.17 → apmtools-0.2.19}/src/apmtools/units.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: apmtools
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.019
|
|
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>
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import threading
|
|
6
6
|
from collections.abc import Sequence
|
|
7
|
-
|
|
7
|
+
import numpy as np
|
|
8
8
|
import pandas as pd
|
|
9
9
|
|
|
10
10
|
|
|
@@ -16,7 +16,6 @@ PLOT_TYPES = {
|
|
|
16
16
|
"scatter": "Scatter",
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
20
19
|
# ``None`` means row-level plotting. The other values are passed directly to
|
|
21
20
|
# Summary.group_stat(frequency=...).
|
|
22
21
|
FREQUENCIES = {
|
|
@@ -100,7 +99,8 @@ def _timedelta_columns(data: pd.DataFrame) -> list[str]:
|
|
|
100
99
|
key=str.casefold,
|
|
101
100
|
)
|
|
102
101
|
|
|
103
|
-
|
|
102
|
+
|
|
103
|
+
def _normalise_y(data: pd.DataFrame, x: str, y: str | Sequence[str] | None) -> list[str]:
|
|
104
104
|
"""Validate and normalise an initial Y selection."""
|
|
105
105
|
numeric = _numeric_columns(data)
|
|
106
106
|
if y is None:
|
|
@@ -179,15 +179,120 @@ def _aggregate(data, value_columns, frequency, grouping_by, aggregate):
|
|
|
179
179
|
return result
|
|
180
180
|
|
|
181
181
|
|
|
182
|
-
def
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
182
|
+
def _histogram_figure(
|
|
183
|
+
data: pd.DataFrame,
|
|
184
|
+
x_column: str,
|
|
185
|
+
*,
|
|
186
|
+
group_column: str | None = None,
|
|
187
|
+
bins: int = 30,
|
|
188
|
+
density: bool = False,
|
|
189
|
+
stacked: bool = False,
|
|
190
|
+
histtype: str = "bar",
|
|
191
|
+
cumulative: int = 0,
|
|
192
|
+
):
|
|
193
|
+
"""Build a Matplotlib-compatible histogram using shared bin boundaries.
|
|
190
194
|
|
|
195
|
+
``cumulative`` is 0 (off), 1 (forward), or -1 (reverse).
|
|
196
|
+
Density for stacked data is normalized across all groups; otherwise each
|
|
197
|
+
group's histogram is normalized independently.
|
|
198
|
+
"""
|
|
199
|
+
import plotly.graph_objects as go
|
|
200
|
+
|
|
201
|
+
if histtype not in {"bar", "barstacked", "step", "stepfilled"}:
|
|
202
|
+
raise ValueError(f"Unknown histtype: {histtype}")
|
|
203
|
+
if cumulative not in {-1, 0, 1}:
|
|
204
|
+
raise ValueError("cumulative must be -1, 0 or 1")
|
|
205
|
+
if bins < 1:
|
|
206
|
+
raise ValueError("bins must be positive")
|
|
207
|
+
|
|
208
|
+
stacked = bool(stacked or histtype == "barstacked")
|
|
209
|
+
labels = (
|
|
210
|
+
list(pd.unique(data[group_column].dropna()))
|
|
211
|
+
if group_column is not None
|
|
212
|
+
else ["All observations"]
|
|
213
|
+
)
|
|
214
|
+
arrays = []
|
|
215
|
+
for label in labels:
|
|
216
|
+
rows = data.loc[data[group_column] == label] if group_column else data
|
|
217
|
+
values = pd.to_numeric(
|
|
218
|
+
rows[x_column], errors="coerce").to_numpy(dtype=float)
|
|
219
|
+
arrays.append(values[np.isfinite(values)])
|
|
220
|
+
|
|
221
|
+
all_values = np.concatenate(arrays) if arrays else np.array([])
|
|
222
|
+
fig = go.Figure()
|
|
223
|
+
if not len(all_values):
|
|
224
|
+
fig.update_layout(title="No numerical observations")
|
|
225
|
+
return fig
|
|
226
|
+
|
|
227
|
+
edges = np.histogram_bin_edges(all_values, bins=bins)
|
|
228
|
+
widths = np.diff(edges)
|
|
229
|
+
counts = np.array([np.histogram(v, bins=edges)[0]
|
|
230
|
+
for v in arrays], dtype=float)
|
|
231
|
+
totals = counts.sum(axis=1)
|
|
232
|
+
grand_total = totals.sum()
|
|
233
|
+
|
|
234
|
+
if density:
|
|
235
|
+
if stacked:
|
|
236
|
+
heights = counts / (grand_total * widths)
|
|
237
|
+
else:
|
|
238
|
+
heights = np.divide(
|
|
239
|
+
counts, totals[:, None] * widths[None, :],
|
|
240
|
+
out=np.zeros_like(counts),
|
|
241
|
+
where=(totals[:, None] > 0),
|
|
242
|
+
)
|
|
243
|
+
else:
|
|
244
|
+
heights = counts.copy()
|
|
245
|
+
|
|
246
|
+
if cumulative:
|
|
247
|
+
# Cumulative density is a CDF/survival curve, not the sum of
|
|
248
|
+
# bin-wise probability densities. It must integrate bin mass.
|
|
249
|
+
mass = counts / grand_total if (density and stacked) else counts.copy()
|
|
250
|
+
if density and not stacked:
|
|
251
|
+
mass = np.divide(
|
|
252
|
+
counts, totals[:, None],
|
|
253
|
+
out=np.zeros_like(counts),
|
|
254
|
+
where=(totals[:, None] > 0),
|
|
255
|
+
)
|
|
256
|
+
heights = (
|
|
257
|
+
np.cumsum(mass, axis=1)
|
|
258
|
+
if cumulative == 1
|
|
259
|
+
else np.flip(np.cumsum(np.flip(mass, axis=1), axis=1), axis=1)
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
for index, label in enumerate(labels):
|
|
263
|
+
values = heights[index]
|
|
264
|
+
if histtype in ("bar", "barstacked"):
|
|
265
|
+
fig.add_trace(go.Bar(
|
|
266
|
+
name=str(label),
|
|
267
|
+
x=(edges[:-1] + edges[1:]) / 2,
|
|
268
|
+
y=values,
|
|
269
|
+
width=widths,
|
|
270
|
+
marker_line_width=0,
|
|
271
|
+
))
|
|
272
|
+
else:
|
|
273
|
+
# Horizontal steps start/end at the shared bin boundaries.
|
|
274
|
+
xx = np.repeat(edges, 2)[1:-1]
|
|
275
|
+
yy = np.repeat(values, 2)
|
|
276
|
+
fig.add_trace(go.Scatter(
|
|
277
|
+
name=str(label),
|
|
278
|
+
x=xx,
|
|
279
|
+
y=yy,
|
|
280
|
+
mode="lines",
|
|
281
|
+
fill="tozeroy" if histtype == "stepfilled" else None,
|
|
282
|
+
stackgroup="histogram" if stacked and histtype == "stepfilled" else None,
|
|
283
|
+
))
|
|
284
|
+
|
|
285
|
+
fig.update_layout(
|
|
286
|
+
barmode="stack" if stacked else "group",
|
|
287
|
+
xaxis_title=x_column,
|
|
288
|
+
yaxis_title=(
|
|
289
|
+
"Cumulative probability" if cumulative and density
|
|
290
|
+
else "Cumulative count" if cumulative
|
|
291
|
+
else "Probability density" if density
|
|
292
|
+
else "Count"
|
|
293
|
+
),
|
|
294
|
+
)
|
|
295
|
+
return fig
|
|
191
296
|
|
|
192
297
|
def _frequency_timedelta(frequency: str) -> pd.Timedelta | None:
|
|
193
298
|
"""Convert a timestamp aggregation frequency to its expected spacing."""
|
|
@@ -300,16 +405,17 @@ def create_summary_app(
|
|
|
300
405
|
columns = _alphabetical(summary.columns)
|
|
301
406
|
column_map = {str(c): c for c in summary.columns}
|
|
302
407
|
numeric_columns = _alphabetical(_numeric_columns(summary))
|
|
303
|
-
categorical_columns =
|
|
304
|
-
datetime_columns =
|
|
305
|
-
|
|
306
|
-
|
|
408
|
+
categorical_columns = _categorical_columns(summary)
|
|
409
|
+
datetime_columns = _datetime_columns(summary)
|
|
410
|
+
group_columns = _alphabetical(
|
|
411
|
+
c for c in columns if not pd.api.types.is_numeric_dtype(summary[column_map[c]])
|
|
412
|
+
)
|
|
307
413
|
|
|
308
414
|
x = _default_x(summary) if x is None else str(x)
|
|
309
415
|
if x not in column_map:
|
|
310
416
|
raise ValueError(f"Unknown x column: {x!r}")
|
|
311
417
|
|
|
312
|
-
initial_y =
|
|
418
|
+
initial_y = _normalise_y(summary, x, y)
|
|
313
419
|
if group_by is None:
|
|
314
420
|
initial_groups = [_default_group(summary)] if _default_group(
|
|
315
421
|
summary) is not None else []
|
|
@@ -320,6 +426,8 @@ def create_summary_app(
|
|
|
320
426
|
unknown_groups = [c for c in initial_groups if c not in column_map]
|
|
321
427
|
if unknown_groups:
|
|
322
428
|
raise ValueError(f"Unknown group_by column(s): {unknown_groups!r}")
|
|
429
|
+
# Numeric columns are not offered by the Group by selector.
|
|
430
|
+
initial_groups = [c for c in initial_groups if c in group_columns]
|
|
323
431
|
|
|
324
432
|
if plot_type not in PLOT_TYPES:
|
|
325
433
|
raise ValueError(
|
|
@@ -336,7 +444,7 @@ def create_summary_app(
|
|
|
336
444
|
ui.output_ui("y_control"),
|
|
337
445
|
ui.input_selectize(
|
|
338
446
|
"group_by", "Group by",
|
|
339
|
-
choices={c: c for c in
|
|
447
|
+
choices={c: c for c in group_columns},
|
|
340
448
|
selected=initial_groups,
|
|
341
449
|
multiple=True,
|
|
342
450
|
options={"placeholder": "Select one or more grouping columns"},
|
|
@@ -373,6 +481,17 @@ def create_summary_app(
|
|
|
373
481
|
"input.plot_type === 'histogram'",
|
|
374
482
|
ui.input_numeric("bins", "Histogram bins",
|
|
375
483
|
value=30, min=1, max=200, step=1),
|
|
484
|
+
ui.input_select("hist_density", "Density", choices={
|
|
485
|
+
"False": "False", "True": "True"}, selected="False"),
|
|
486
|
+
ui.input_select("hist_stacked", "Stacked", choices={
|
|
487
|
+
"False": "False", "True": "True"}, selected="False"),
|
|
488
|
+
ui.input_select("hist_type", "Histogram type", choices={
|
|
489
|
+
"bar": "bar", "barstacked": "barstacked",
|
|
490
|
+
"step": "step", "stepfilled": "stepfilled",
|
|
491
|
+
}, selected="bar"),
|
|
492
|
+
ui.input_select("hist_cumulative", "Cumulative", choices={
|
|
493
|
+
"0": "False", "1": "True (ascending)", "-1": "-1 (descending)",
|
|
494
|
+
}, selected="0"),
|
|
376
495
|
),
|
|
377
496
|
ui.input_text("title", "Plot title", value=title),
|
|
378
497
|
ui.help_text(
|
|
@@ -595,13 +714,15 @@ def create_summary_app(
|
|
|
595
714
|
)
|
|
596
715
|
|
|
597
716
|
if selected_plot == "histogram":
|
|
598
|
-
fig =
|
|
717
|
+
fig = _histogram_figure(
|
|
599
718
|
plot_df,
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
719
|
+
x_column,
|
|
720
|
+
group_column="__plot_group" if group_columns else None,
|
|
721
|
+
bins=int(input.bins()),
|
|
722
|
+
density=input.hist_density() == "True",
|
|
723
|
+
stacked=input.hist_stacked() == "True",
|
|
724
|
+
histtype=input.hist_type(),
|
|
725
|
+
cumulative=int(input.hist_cumulative()),
|
|
605
726
|
)
|
|
606
727
|
elif selected_plot == "box":
|
|
607
728
|
fig = px.box(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|