apmtools 0.2.18__tar.gz → 0.2.20__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.18 → apmtools-0.2.20}/PKG-INFO +1 -1
- {apmtools-0.2.18 → apmtools-0.2.20}/pyproject.toml +1 -1
- {apmtools-0.2.18 → apmtools-0.2.20}/src/apmtools/shiny/summary.py +143 -11
- {apmtools-0.2.18 → apmtools-0.2.20}/.gitignore +0 -0
- {apmtools-0.2.18 → apmtools-0.2.20}/LICENSE +0 -0
- {apmtools-0.2.18 → apmtools-0.2.20}/README.md +0 -0
- {apmtools-0.2.18 → apmtools-0.2.20}/src/apmtools/__init__.py +0 -0
- {apmtools-0.2.18 → apmtools-0.2.20}/src/apmtools/classes.py +0 -0
- {apmtools-0.2.18 → apmtools-0.2.20}/src/apmtools/data_processing.py +0 -0
- {apmtools-0.2.18 → apmtools-0.2.20}/src/apmtools/functions.py +0 -0
- {apmtools-0.2.18 → apmtools-0.2.20}/src/apmtools/legacy.py +0 -0
- {apmtools-0.2.18 → apmtools-0.2.20}/src/apmtools/shiny/__init__.py +0 -0
- {apmtools-0.2.18 → apmtools-0.2.20}/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.020
|
|
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
|
|
|
@@ -179,6 +179,121 @@ def _aggregate(data, value_columns, frequency, grouping_by, aggregate):
|
|
|
179
179
|
return result
|
|
180
180
|
|
|
181
181
|
|
|
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.
|
|
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
|
|
296
|
+
|
|
182
297
|
def _frequency_timedelta(frequency: str) -> pd.Timedelta | None:
|
|
183
298
|
"""Convert a timestamp aggregation frequency to its expected spacing."""
|
|
184
299
|
if not frequency or frequency == "weekday":
|
|
@@ -366,6 +481,17 @@ def create_summary_app(
|
|
|
366
481
|
"input.plot_type === 'histogram'",
|
|
367
482
|
ui.input_numeric("bins", "Histogram bins",
|
|
368
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"),
|
|
369
495
|
),
|
|
370
496
|
ui.input_text("title", "Plot title", value=title),
|
|
371
497
|
ui.help_text(
|
|
@@ -374,10 +500,13 @@ def create_summary_app(
|
|
|
374
500
|
"Summary.group_stat()."
|
|
375
501
|
),
|
|
376
502
|
),
|
|
377
|
-
ui.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
503
|
+
ui.div(
|
|
504
|
+
output_widget("plot", height="100%", width="100%"),
|
|
505
|
+
style=(
|
|
506
|
+
"width: 100%; max-width: 100%; aspect-ratio: 16 / 9; "
|
|
507
|
+
"min-height: 0; margin: 0; padding: 0;"
|
|
508
|
+
),
|
|
509
|
+
),
|
|
381
510
|
)
|
|
382
511
|
|
|
383
512
|
def server(input, output, session):
|
|
@@ -588,13 +717,15 @@ def create_summary_app(
|
|
|
588
717
|
)
|
|
589
718
|
|
|
590
719
|
if selected_plot == "histogram":
|
|
591
|
-
fig =
|
|
720
|
+
fig = _histogram_figure(
|
|
592
721
|
plot_df,
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
722
|
+
x_column,
|
|
723
|
+
group_column="__plot_group" if group_columns else None,
|
|
724
|
+
bins=int(input.bins()),
|
|
725
|
+
density=input.hist_density() == "True",
|
|
726
|
+
stacked=input.hist_stacked() == "True",
|
|
727
|
+
histtype=input.hist_type(),
|
|
728
|
+
cumulative=int(input.hist_cumulative()),
|
|
598
729
|
)
|
|
599
730
|
elif selected_plot == "box":
|
|
600
731
|
fig = px.box(
|
|
@@ -728,6 +859,7 @@ def create_summary_app(
|
|
|
728
859
|
title=input.title(),
|
|
729
860
|
hovermode="x unified" if selected_plot == "line" else "closest",
|
|
730
861
|
margin=dict(l=60, r=30, t=70, b=60),
|
|
862
|
+
autosize=True,
|
|
731
863
|
)
|
|
732
864
|
return fig
|
|
733
865
|
|
|
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
|