apmtools 0.2.39__tar.gz → 0.2.41__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: apmtools
3
- Version: 0.2.39
3
+ Version: 0.2.41
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 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "apmtools"
7
- version = "0.2.39"
7
+ version = "0.2.41"
8
8
  authors = [
9
9
  { name="Federico Lorenzetti", email="lorenzetti.federico@gmail.com" },
10
10
  ]
@@ -1161,7 +1161,7 @@ class Summary(pd.DataFrame):
1161
1161
  if attribute in self.columns:
1162
1162
  return set(self[attribute])
1163
1163
 
1164
- def group_stat(self, value_cols, frequency=None, grouping_by=["identifier"], time_col='time', id_col='identifier',
1164
+ def group_stat(self, value_cols, frequency=None, grouping_by=["identifier"], time_col='datetime', id_col='identifier',
1165
1165
  min_completeness=1.0, only_complete=True, flooring_timestamp=True, aggregate=["mean"]):
1166
1166
  """
1167
1167
  Filter a DataFrame down to only the (monitor, hour) groups that have
@@ -36,7 +36,7 @@ def _axis_label(variable: str) -> str:
36
36
 
37
37
 
38
38
  def _label_plot_axes(fig, plot_type, x_column, y_columns, *,
39
- category_columns=None):
39
+ category_columns=None, x_label_override=None):
40
40
  """Label plot axes while retaining special histogram and facet labels."""
41
41
  if plot_type == "histogram":
42
42
  fig.update_xaxes(title_text=_axis_label(x_column))
@@ -58,6 +58,8 @@ def _label_plot_axes(fig, plot_type, x_column, y_columns, *,
58
58
  fig.update_xaxes(title_text=_axis_label(x_column))
59
59
  if y_columns:
60
60
  fig.update_yaxes(title_text=_axis_label(y_columns[0]))
61
+ if x_label_override is not None:
62
+ fig.update_xaxes(title_text=x_label_override)
61
63
  return fig
62
64
 
63
65
 
@@ -947,6 +949,7 @@ def _matplotlib_defaults(spec, *, palette="Default"):
947
949
  y_label = ("Probability density" if kind == "histogram" else
948
950
  "Value" if kind == "line" and len(ys) > 1 else
949
951
  _axis_label(ys[0]) if ys else "Count")
952
+ x_label = spec.get("x_label_override") or x_label
950
953
  return dict(x_label=x_label, y_label=y_label, names=names, colors=colors)
951
954
 
952
955
 
@@ -1681,7 +1684,6 @@ def create_summary_app(
1681
1684
  categorical_columns = _categorical_columns(summary)
1682
1685
  datetime_columns = _datetime_columns(summary)
1683
1686
  timedelta_columns = _timedelta_columns(summary)
1684
-
1685
1687
  time_group_columns = [f for f in FREQUENCIES.values(
1686
1688
  ) if f is not None and f not in column_map]
1687
1689
  group_columns = _alphabetical(
@@ -1988,7 +1990,8 @@ def create_summary_app(
1988
1990
  choices = categorical_columns
1989
1991
  multiple = True
1990
1992
  elif kind == "line":
1991
- choices = datetime_columns+timedelta_columns
1993
+ choices = _alphabetical(
1994
+ datetime_columns + timedelta_columns)
1992
1995
  multiple = False
1993
1996
  else:
1994
1997
  choices = numeric_columns
@@ -2133,6 +2136,12 @@ def create_summary_app(
2133
2136
  value_columns = list(dict.fromkeys(
2134
2137
  [column_map[selected_x[0]], *value_columns]
2135
2138
  ))
2139
+ if (kind == "line" and selected_x[0] in timedelta_columns
2140
+ and should_aggregate):
2141
+ # group_stat aggregation drops the original timedelta X
2142
+ # unless it is explicitly aggregated. Never silently plot
2143
+ # an unrelated frequency column instead.
2144
+ return pd.DataFrame()
2136
2145
  if should_aggregate:
2137
2146
  current = _aggregate(
2138
2147
  current, value_columns, frequency, grouping_by, aggregate,
@@ -2158,6 +2167,10 @@ def create_summary_app(
2158
2167
  should_aggregate = AGGREGATES.get(
2159
2168
  input.aggregate()) is not None
2160
2169
 
2170
+ if (selected_plot == "line" and selected_x
2171
+ and selected_x[0] in timedelta_columns
2172
+ and should_aggregate):
2173
+ return {"error": "Timedelta X-axis requires Aggregation: None. Disable aggregation to plot elapsed time."}
2161
2174
  if plot_df.empty or not selected_x or (
2162
2175
  selected_plot != "histogram" and not selected_y
2163
2176
  ):
@@ -2180,7 +2193,9 @@ def create_summary_app(
2180
2193
  x_column = mapped(selected_x[0])
2181
2194
  y_columns = [mapped(selected_y[0])]
2182
2195
  else:
2183
- x_column = frequency if frequency is not None else column_map[selected_x[0]]
2196
+ x_column = (column_map[selected_x[0]] if selected_x[0] in timedelta_columns
2197
+ else frequency if frequency is not None
2198
+ else column_map[selected_x[0]])
2184
2199
  y_columns = [mapped(c) for c in selected_y]
2185
2200
 
2186
2201
  if selected_plot == "line" and should_aggregate and frequency is None:
@@ -2198,6 +2213,20 @@ def create_summary_app(
2198
2213
  plot_df, x_columns)
2199
2214
  x_column = "__plot_category"
2200
2215
 
2216
+ # Convert durations once for Plotly, Matplotlib, editable
2217
+ # numeric bounds, faceting and exported plots. Retain the
2218
+ # original variable name and label its displayed unit.
2219
+ x_label_override = None
2220
+ if (selected_plot == "line" and
2221
+ pd.api.types.is_timedelta64_dtype(plot_df[x_column])):
2222
+ plot_df = plot_df.copy()
2223
+ plot_df[x_column] = plot_df[x_column] / \
2224
+ pd.Timedelta(hours=1)
2225
+ x_label_override = f"{x_column} [hours]"
2226
+ # Frequency-based gap detection is only meaningful for
2227
+ # aggregated timestamp series, not raw durations.
2228
+ frequency = None
2229
+
2201
2230
  # Multiple grouping columns are supported by group_stat(). For
2202
2231
  # plotting, Plotly needs one color/category column, so combine the
2203
2232
  # selected grouping columns into a readable composite label.
@@ -2212,7 +2241,7 @@ def create_summary_app(
2212
2241
  return {"data": plot_df, "kind": selected_plot, "x": x_column,
2213
2242
  "ys": y_columns, "groups": group_columns,
2214
2243
  "categories": x_columns if selected_plot in ("box", "bar") else None,
2215
- "frequency": frequency}
2244
+ "frequency": frequency, "x_label_override": x_label_override}
2216
2245
 
2217
2246
  @reactive.calc
2218
2247
  def base_figure():
@@ -2260,6 +2289,7 @@ def create_summary_app(
2260
2289
  fig, selected_plot, x_column, y_columns,
2261
2290
  category_columns=x_columns if selected_plot in (
2262
2291
  "box", "bar") else None,
2292
+ x_label_override=spec.get("x_label_override"),
2263
2293
  )
2264
2294
  fig.update_layout(
2265
2295
  hovermode="x unified" if selected_plot == "line" else "closest",
File without changes
File without changes
File without changes