apmtools 0.2.9__tar.gz → 0.2.10__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.9 → apmtools-0.2.10}/PKG-INFO +1 -1
- {apmtools-0.2.9 → apmtools-0.2.10}/pyproject.toml +1 -1
- {apmtools-0.2.9 → apmtools-0.2.10}/src/apmtools/shiny/summary.py +127 -3
- {apmtools-0.2.9 → apmtools-0.2.10}/.gitignore +0 -0
- {apmtools-0.2.9 → apmtools-0.2.10}/LICENSE +0 -0
- {apmtools-0.2.9 → apmtools-0.2.10}/README.md +0 -0
- {apmtools-0.2.9 → apmtools-0.2.10}/src/apmtools/__init__.py +0 -0
- {apmtools-0.2.9 → apmtools-0.2.10}/src/apmtools/classes.py +0 -0
- {apmtools-0.2.9 → apmtools-0.2.10}/src/apmtools/data_processing.py +0 -0
- {apmtools-0.2.9 → apmtools-0.2.10}/src/apmtools/functions.py +0 -0
- {apmtools-0.2.9 → apmtools-0.2.10}/src/apmtools/legacy.py +0 -0
- {apmtools-0.2.9 → apmtools-0.2.10}/src/apmtools/shiny/__init__.py +0 -0
- {apmtools-0.2.9 → apmtools-0.2.10}/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.010
|
|
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>
|
|
@@ -142,6 +142,91 @@ def _aggregated_value_columns(data, value_columns, aggregate):
|
|
|
142
142
|
return columns
|
|
143
143
|
|
|
144
144
|
|
|
145
|
+
def _frequency_timedelta(frequency: str) -> pd.Timedelta | None:
|
|
146
|
+
"""Convert a timestamp aggregation frequency to its expected spacing."""
|
|
147
|
+
if not frequency or frequency == "weekday":
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
parts = frequency.split()
|
|
151
|
+
if len(parts) == 1:
|
|
152
|
+
multiplier = 1.0
|
|
153
|
+
unit = parts[0]
|
|
154
|
+
elif len(parts) == 2:
|
|
155
|
+
try:
|
|
156
|
+
multiplier = float(parts[0])
|
|
157
|
+
except ValueError:
|
|
158
|
+
return None
|
|
159
|
+
unit = parts[1]
|
|
160
|
+
else:
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
units = {
|
|
164
|
+
"second": "s",
|
|
165
|
+
"minute": "min",
|
|
166
|
+
"hour": "h",
|
|
167
|
+
"day": "d",
|
|
168
|
+
"week": "W",
|
|
169
|
+
}
|
|
170
|
+
if unit not in units:
|
|
171
|
+
return None
|
|
172
|
+
return pd.to_timedelta(multiplier, unit=units[unit])
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _add_gap_sections(
|
|
176
|
+
data: pd.DataFrame,
|
|
177
|
+
*,
|
|
178
|
+
x_column: str,
|
|
179
|
+
group_columns: list[str],
|
|
180
|
+
frequency: str,
|
|
181
|
+
section_column: str = "__line_section",
|
|
182
|
+
) -> pd.DataFrame:
|
|
183
|
+
"""Assign separate line sections when aggregated observations have gaps.
|
|
184
|
+
|
|
185
|
+
Rows remain in the same section while adjacent timestamps are no farther
|
|
186
|
+
apart than the selected aggregation frequency. Section numbering restarts
|
|
187
|
+
independently inside every selected plotting group.
|
|
188
|
+
|
|
189
|
+
This is primarily used when ``identifier`` was not included in
|
|
190
|
+
``group_stat(grouping_by=...)`` and is therefore no longer available after
|
|
191
|
+
aggregation.
|
|
192
|
+
"""
|
|
193
|
+
result = data.copy()
|
|
194
|
+
if result.empty:
|
|
195
|
+
result[section_column] = pd.Series(dtype="string")
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
step = _frequency_timedelta(frequency)
|
|
199
|
+
if step is None or x_column not in result.columns:
|
|
200
|
+
# Frequencies such as weekday do not preserve enough chronological
|
|
201
|
+
# information to infer discontinuities reliably.
|
|
202
|
+
result[section_column] = "0"
|
|
203
|
+
return result
|
|
204
|
+
|
|
205
|
+
result[x_column] = pd.to_datetime(result[x_column], errors="coerce")
|
|
206
|
+
sort_columns = [*group_columns, x_column]
|
|
207
|
+
result = result.sort_values(sort_columns, kind="stable").copy()
|
|
208
|
+
|
|
209
|
+
if group_columns:
|
|
210
|
+
gap = result.groupby(group_columns, dropna=False)[x_column].diff()
|
|
211
|
+
new_section = gap.gt(step) | gap.isna()
|
|
212
|
+
section_number = (
|
|
213
|
+
new_section.astype(int)
|
|
214
|
+
.groupby([result[c] for c in group_columns], dropna=False)
|
|
215
|
+
.cumsum()
|
|
216
|
+
.sub(1)
|
|
217
|
+
)
|
|
218
|
+
group_label = result[group_columns].astype(str).agg(" | ".join, axis=1)
|
|
219
|
+
result[section_column] = group_label + \
|
|
220
|
+
" | section " + section_number.astype(str)
|
|
221
|
+
else:
|
|
222
|
+
gap = result[x_column].diff()
|
|
223
|
+
section_number = (gap.gt(step) | gap.isna()
|
|
224
|
+
).astype(int).cumsum().sub(1)
|
|
225
|
+
result[section_column] = "section " + section_number.astype(str)
|
|
226
|
+
|
|
227
|
+
return result
|
|
228
|
+
|
|
229
|
+
|
|
145
230
|
def create_summary_app(
|
|
146
231
|
data: pd.DataFrame,
|
|
147
232
|
*,
|
|
@@ -459,13 +544,29 @@ def create_summary_app(
|
|
|
459
544
|
# being connected by a line merely because they share the
|
|
460
545
|
# same selected group. The identifier is deliberately NOT
|
|
461
546
|
# used for colour, so monitors in the same selected group
|
|
462
|
-
# retain the same colour.
|
|
547
|
+
# retain the same colour. After frequency aggregation,
|
|
548
|
+
# identifier is only available if it was selected as a
|
|
549
|
+
# grouping variable. If it is absent, discontinuous runs are
|
|
550
|
+
# inferred from gaps larger than the selected frequency.
|
|
463
551
|
identifier_column = (
|
|
464
552
|
column_map["identifier"]
|
|
465
|
-
if
|
|
553
|
+
if (
|
|
554
|
+
"identifier" in column_map
|
|
555
|
+
and column_map["identifier"] in plot_df.columns
|
|
556
|
+
)
|
|
466
557
|
else None
|
|
467
558
|
)
|
|
468
559
|
|
|
560
|
+
section_column = None
|
|
561
|
+
if frequency is not None and identifier_column is None:
|
|
562
|
+
plot_df = _add_gap_sections(
|
|
563
|
+
plot_df,
|
|
564
|
+
x_column=x_column,
|
|
565
|
+
group_columns=group_columns,
|
|
566
|
+
frequency=frequency,
|
|
567
|
+
)
|
|
568
|
+
section_column = "__line_section"
|
|
569
|
+
|
|
469
570
|
if group_columns:
|
|
470
571
|
group_column = "__plot_group"
|
|
471
572
|
line_columns = [x_column, group_column, *y_columns]
|
|
@@ -486,7 +587,7 @@ def create_summary_app(
|
|
|
486
587
|
x=x_column,
|
|
487
588
|
y="value",
|
|
488
589
|
color=group_column,
|
|
489
|
-
line_group=identifier_column
|
|
590
|
+
line_group=identifier_column or section_column,
|
|
490
591
|
facet_row="variable" if len(y_columns) > 1 else None,
|
|
491
592
|
)
|
|
492
593
|
elif identifier_column is not None:
|
|
@@ -516,6 +617,29 @@ def create_summary_app(
|
|
|
516
617
|
line_group=identifier_column,
|
|
517
618
|
facet_row="variable",
|
|
518
619
|
)
|
|
620
|
+
elif section_column is not None:
|
|
621
|
+
if len(y_columns) == 1:
|
|
622
|
+
fig = px.line(
|
|
623
|
+
plot_df,
|
|
624
|
+
x=x_column,
|
|
625
|
+
y=y_columns[0],
|
|
626
|
+
line_group=section_column,
|
|
627
|
+
)
|
|
628
|
+
else:
|
|
629
|
+
line_columns = [x_column, section_column, *y_columns]
|
|
630
|
+
long_df = plot_df[line_columns].melt(
|
|
631
|
+
id_vars=[x_column, section_column],
|
|
632
|
+
var_name="variable",
|
|
633
|
+
value_name="value",
|
|
634
|
+
)
|
|
635
|
+
fig = px.line(
|
|
636
|
+
long_df,
|
|
637
|
+
x=x_column,
|
|
638
|
+
y="value",
|
|
639
|
+
color="variable",
|
|
640
|
+
line_group=section_column,
|
|
641
|
+
facet_row="variable",
|
|
642
|
+
)
|
|
519
643
|
else:
|
|
520
644
|
fig = px.line(plot_df, x=x_column, y=y_columns)
|
|
521
645
|
|
|
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
|