google-meridian 1.0.8__tar.gz → 1.0.9__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.
- {google_meridian-1.0.8/google_meridian.egg-info → google_meridian-1.0.9}/PKG-INFO +2 -2
- {google_meridian-1.0.8 → google_meridian-1.0.9}/README.md +1 -1
- {google_meridian-1.0.8 → google_meridian-1.0.9/google_meridian.egg-info}/PKG-INFO +2 -2
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/__init__.py +1 -1
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/analyzer.py +108 -18
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/optimizer.py +196 -45
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/summarizer.py +21 -3
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/visualizer.py +69 -23
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/constants.py +12 -11
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/model.py +15 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/prior_distribution.py +22 -1
- {google_meridian-1.0.8 → google_meridian-1.0.9}/LICENSE +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/MANIFEST.in +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/google_meridian.egg-info/SOURCES.txt +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/google_meridian.egg-info/dependency_links.txt +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/google_meridian.egg-info/requires.txt +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/google_meridian.egg-info/top_level.txt +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/__init__.py +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/formatter.py +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/summary_text.py +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/templates/card.html.jinja +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/templates/chart.html.jinja +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/templates/chips.html.jinja +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/templates/insights.html.jinja +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/templates/stats.html.jinja +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/templates/style.scss +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/templates/summary.html.jinja +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/templates/table.html.jinja +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/analysis/test_utils.py +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/data/__init__.py +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/data/arg_builder.py +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/data/input_data.py +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/data/load.py +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/data/test_utils.py +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/data/time_coordinates.py +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/__init__.py +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/adstock_hill.py +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/knots.py +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/media.py +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/model_test_data.py +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/posterior_sampler.py +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/prior_sampler.py +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/spec.py +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/meridian/model/transformers.py +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/pyproject.toml +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/setup.cfg +0 -0
- {google_meridian-1.0.8 → google_meridian-1.0.9}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: google-meridian
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.9
|
|
4
4
|
Summary: Google's open source mixed marketing model library, helps you understand your return on investment and direct your ad spend with confidence.
|
|
5
5
|
Author-email: The Meridian Authors <no-reply@google.com>
|
|
6
6
|
License:
|
|
@@ -393,7 +393,7 @@ To cite this repository:
|
|
|
393
393
|
author = {Google Meridian Marketing Mix Modeling Team},
|
|
394
394
|
title = {Meridian: Marketing Mix Modeling},
|
|
395
395
|
url = {https://github.com/google/meridian},
|
|
396
|
-
version = {1.0.
|
|
396
|
+
version = {1.0.9},
|
|
397
397
|
year = {2025},
|
|
398
398
|
}
|
|
399
399
|
```
|
|
@@ -151,7 +151,7 @@ To cite this repository:
|
|
|
151
151
|
author = {Google Meridian Marketing Mix Modeling Team},
|
|
152
152
|
title = {Meridian: Marketing Mix Modeling},
|
|
153
153
|
url = {https://github.com/google/meridian},
|
|
154
|
-
version = {1.0.
|
|
154
|
+
version = {1.0.9},
|
|
155
155
|
year = {2025},
|
|
156
156
|
}
|
|
157
157
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: google-meridian
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.9
|
|
4
4
|
Summary: Google's open source mixed marketing model library, helps you understand your return on investment and direct your ad spend with confidence.
|
|
5
5
|
Author-email: The Meridian Authors <no-reply@google.com>
|
|
6
6
|
License:
|
|
@@ -393,7 +393,7 @@ To cite this repository:
|
|
|
393
393
|
author = {Google Meridian Marketing Mix Modeling Team},
|
|
394
394
|
title = {Meridian: Marketing Mix Modeling},
|
|
395
395
|
url = {https://github.com/google/meridian},
|
|
396
|
-
version = {1.0.
|
|
396
|
+
version = {1.0.9},
|
|
397
397
|
year = {2025},
|
|
398
398
|
}
|
|
399
399
|
```
|
|
@@ -63,6 +63,8 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
63
63
|
controls: Optional tensor with dimensions `(n_geos, n_times, n_controls)`.
|
|
64
64
|
revenue_per_kpi: Optional tensor with dimensions `(n_geos, T)` for any time
|
|
65
65
|
dimension `T`.
|
|
66
|
+
time: Optional tensor of time coordinates in the "YYYY-mm-dd" string format
|
|
67
|
+
for time dimension `T`.
|
|
66
68
|
"""
|
|
67
69
|
|
|
68
70
|
media: Optional[tf.Tensor]
|
|
@@ -76,6 +78,7 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
76
78
|
non_media_treatments: Optional[tf.Tensor]
|
|
77
79
|
controls: Optional[tf.Tensor]
|
|
78
80
|
revenue_per_kpi: Optional[tf.Tensor]
|
|
81
|
+
time: Optional[tf.Tensor]
|
|
79
82
|
|
|
80
83
|
def __init__(
|
|
81
84
|
self,
|
|
@@ -90,6 +93,7 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
90
93
|
non_media_treatments: Optional[tf.Tensor] = None,
|
|
91
94
|
controls: Optional[tf.Tensor] = None,
|
|
92
95
|
revenue_per_kpi: Optional[tf.Tensor] = None,
|
|
96
|
+
time: Optional[Sequence[str] | tf.Tensor] = None,
|
|
93
97
|
):
|
|
94
98
|
self.media = tf.cast(media, tf.float32) if media is not None else None
|
|
95
99
|
self.media_spend = (
|
|
@@ -130,6 +134,7 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
130
134
|
if revenue_per_kpi is not None
|
|
131
135
|
else None
|
|
132
136
|
)
|
|
137
|
+
self.time = tf.cast(time, tf.string) if time is not None else None
|
|
133
138
|
|
|
134
139
|
def __validate__(self):
|
|
135
140
|
self._validate_n_dims()
|
|
@@ -241,6 +246,8 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
241
246
|
f"New `{field.name}` must have 1 or 3 dimensions. Found"
|
|
242
247
|
f" {tensor.ndim} dimensions."
|
|
243
248
|
)
|
|
249
|
+
elif field.name == constants.TIME:
|
|
250
|
+
_check_n_dims(tensor, field.name, 1)
|
|
244
251
|
else:
|
|
245
252
|
_check_n_dims(tensor, field.name, 3)
|
|
246
253
|
|
|
@@ -283,7 +290,7 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
283
290
|
for var_name in required_fields:
|
|
284
291
|
new_tensor = getattr(self, var_name)
|
|
285
292
|
if new_tensor is not None and new_tensor.shape[0] != meridian.n_geos:
|
|
286
|
-
# Skip spend data with only 1 dimension
|
|
293
|
+
# Skip spend and time data with only 1 dimension.
|
|
287
294
|
if new_tensor.ndim == 1:
|
|
288
295
|
continue
|
|
289
296
|
raise ValueError(
|
|
@@ -296,7 +303,7 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
296
303
|
):
|
|
297
304
|
"""Validates the channel dimension of the specified data variables."""
|
|
298
305
|
for var_name in required_fields:
|
|
299
|
-
if var_name
|
|
306
|
+
if var_name in [constants.REVENUE_PER_KPI, constants.TIME]:
|
|
300
307
|
continue
|
|
301
308
|
new_tensor = getattr(self, var_name)
|
|
302
309
|
old_tensor = getattr(meridian.input_data, var_name)
|
|
@@ -317,12 +324,24 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
317
324
|
old_tensor = getattr(meridian.input_data, var_name)
|
|
318
325
|
|
|
319
326
|
# Skip spend data with only 1 dimension of (n_channels).
|
|
320
|
-
if
|
|
327
|
+
if (
|
|
328
|
+
var_name in [constants.MEDIA_SPEND, constants.RF_SPEND]
|
|
329
|
+
and new_tensor is not None
|
|
330
|
+
and new_tensor.ndim == 1
|
|
331
|
+
):
|
|
321
332
|
continue
|
|
322
333
|
|
|
323
334
|
if new_tensor is not None:
|
|
324
335
|
assert old_tensor is not None
|
|
325
|
-
if
|
|
336
|
+
if (
|
|
337
|
+
var_name == constants.TIME
|
|
338
|
+
and new_tensor.shape[0] != old_tensor.shape[0]
|
|
339
|
+
):
|
|
340
|
+
raise ValueError(
|
|
341
|
+
f"New `{var_name}` is expected to have {old_tensor.shape[0]}"
|
|
342
|
+
f" time periods. Found {new_tensor.shape[0]} time periods."
|
|
343
|
+
)
|
|
344
|
+
elif new_tensor.ndim > 1 and new_tensor.shape[1] != old_tensor.shape[1]:
|
|
326
345
|
raise ValueError(
|
|
327
346
|
f"New `{var_name}` is expected to have {old_tensor.shape[1]}"
|
|
328
347
|
f" time periods. Found {new_tensor.shape[1]} time periods."
|
|
@@ -345,12 +364,24 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
345
364
|
if old_tensor is None:
|
|
346
365
|
continue
|
|
347
366
|
# Skip spend data with only 1 dimension of (n_channels).
|
|
348
|
-
if
|
|
367
|
+
if (
|
|
368
|
+
var_name in [constants.MEDIA_SPEND, constants.RF_SPEND]
|
|
369
|
+
and new_tensor is not None
|
|
370
|
+
and new_tensor.ndim == 1
|
|
371
|
+
):
|
|
349
372
|
continue
|
|
350
373
|
|
|
351
374
|
if new_tensor is None:
|
|
352
375
|
missing_params.append(var_name)
|
|
353
|
-
elif new_tensor.shape[
|
|
376
|
+
elif var_name == constants.TIME and new_tensor.shape[0] != new_n_times:
|
|
377
|
+
raise ValueError(
|
|
378
|
+
"If the time dimension of any variable in `new_data` is "
|
|
379
|
+
"modified, then all variables must be provided with the same "
|
|
380
|
+
f"number of time periods. `{var_name}` has {new_tensor.shape[1]} "
|
|
381
|
+
"time periods, which does not match the modified number of time "
|
|
382
|
+
f"periods, {new_n_times}.",
|
|
383
|
+
)
|
|
384
|
+
elif new_tensor.ndim > 1 and new_tensor.shape[1] != new_n_times:
|
|
354
385
|
raise ValueError(
|
|
355
386
|
"If the time dimension of any variable in `new_data` is "
|
|
356
387
|
"modified, then all variables must be provided with the same "
|
|
@@ -390,6 +421,10 @@ class DataTensors(tf.experimental.ExtensionType):
|
|
|
390
421
|
old_tensor = meridian.controls
|
|
391
422
|
elif var_name == constants.REVENUE_PER_KPI:
|
|
392
423
|
old_tensor = meridian.revenue_per_kpi
|
|
424
|
+
elif var_name == constants.TIME:
|
|
425
|
+
old_tensor = tf.convert_to_tensor(
|
|
426
|
+
meridian.input_data.time.values.tolist(), dtype=tf.string
|
|
427
|
+
)
|
|
393
428
|
else:
|
|
394
429
|
continue
|
|
395
430
|
|
|
@@ -4663,11 +4698,11 @@ class Analyzer:
|
|
|
4663
4698
|
|
|
4664
4699
|
def get_historical_spend(
|
|
4665
4700
|
self,
|
|
4666
|
-
selected_times: Sequence[str] | None,
|
|
4701
|
+
selected_times: Sequence[str] | None = None,
|
|
4667
4702
|
include_media: bool = True,
|
|
4668
4703
|
include_rf: bool = True,
|
|
4669
4704
|
) -> xr.DataArray:
|
|
4670
|
-
"""Gets the aggregated historical spend based on the time
|
|
4705
|
+
"""Deprecated. Gets the aggregated historical spend based on the time.
|
|
4671
4706
|
|
|
4672
4707
|
Args:
|
|
4673
4708
|
selected_times: The time period to get the historical spends. If None, the
|
|
@@ -4681,6 +4716,51 @@ class Analyzer:
|
|
|
4681
4716
|
An `xr.DataArray` with the coordinate `channel` and contains the data
|
|
4682
4717
|
variable `spend`.
|
|
4683
4718
|
|
|
4719
|
+
Raises:
|
|
4720
|
+
ValueError: A ValueError is raised when `include_media` and `include_rf`
|
|
4721
|
+
are both False.
|
|
4722
|
+
"""
|
|
4723
|
+
warnings.warn(
|
|
4724
|
+
"`get_historical_spend` is deprecated. Please use "
|
|
4725
|
+
"`get_aggregated_spend` with `new_data=None` instead.",
|
|
4726
|
+
DeprecationWarning,
|
|
4727
|
+
stacklevel=2,
|
|
4728
|
+
)
|
|
4729
|
+
return self.get_aggregated_spend(
|
|
4730
|
+
selected_times=selected_times,
|
|
4731
|
+
include_media=include_media,
|
|
4732
|
+
include_rf=include_rf,
|
|
4733
|
+
)
|
|
4734
|
+
|
|
4735
|
+
def get_aggregated_spend(
|
|
4736
|
+
self,
|
|
4737
|
+
new_data: DataTensors | None = None,
|
|
4738
|
+
selected_times: Sequence[str] | Sequence[bool] | None = None,
|
|
4739
|
+
include_media: bool = True,
|
|
4740
|
+
include_rf: bool = True,
|
|
4741
|
+
) -> xr.DataArray:
|
|
4742
|
+
"""Gets the aggregated spend based on the selected time.
|
|
4743
|
+
|
|
4744
|
+
Args:
|
|
4745
|
+
new_data: An optional `DataTensors` object containing the new `media`,
|
|
4746
|
+
`media_spend`, `reach`, `frequency`, `rf_spend` tensors. If `None`, the
|
|
4747
|
+
existing tensors from the Meridian object are used. If `new_data`
|
|
4748
|
+
argument is used, then the aggregated spend is computed using the values
|
|
4749
|
+
of the tensors passed in the `new_data` argument and the original values
|
|
4750
|
+
of all the remaining tensors. If any of the tensors in `new_data` is
|
|
4751
|
+
provided with a different number of time periods than in `InputData`,
|
|
4752
|
+
then all tensors must be provided with the same number of time periods.
|
|
4753
|
+
selected_times: The time period to get the aggregated spends. If None, the
|
|
4754
|
+
spend will be aggregated over all time periods.
|
|
4755
|
+
include_media: Whether to include spends for paid media channels that do
|
|
4756
|
+
not have R&F data.
|
|
4757
|
+
include_rf: Whether to include spends for paid media channels with R&F
|
|
4758
|
+
data.
|
|
4759
|
+
|
|
4760
|
+
Returns:
|
|
4761
|
+
An `xr.DataArray` with the coordinate `channel` and contains the data
|
|
4762
|
+
variable `spend`.
|
|
4763
|
+
|
|
4684
4764
|
Raises:
|
|
4685
4765
|
ValueError: A ValueError is raised when `include_media` and `include_rf`
|
|
4686
4766
|
are both False.
|
|
@@ -4689,6 +4769,11 @@ class Analyzer:
|
|
|
4689
4769
|
raise ValueError(
|
|
4690
4770
|
"At least one of include_media or include_rf must be True."
|
|
4691
4771
|
)
|
|
4772
|
+
new_data = new_data or DataTensors()
|
|
4773
|
+
required_tensors_names = constants.PAID_CHANNELS + constants.SPEND_DATA
|
|
4774
|
+
filled_data = new_data.validate_and_fill_missing_data(
|
|
4775
|
+
required_tensors_names, self._meridian
|
|
4776
|
+
)
|
|
4692
4777
|
|
|
4693
4778
|
empty_da = xr.DataArray(
|
|
4694
4779
|
dims=[constants.CHANNEL], coords={constants.CHANNEL: []}
|
|
@@ -4709,8 +4794,8 @@ class Analyzer:
|
|
|
4709
4794
|
else:
|
|
4710
4795
|
aggregated_media_spend = self._impute_and_aggregate_spend(
|
|
4711
4796
|
selected_times,
|
|
4712
|
-
|
|
4713
|
-
|
|
4797
|
+
filled_data.media,
|
|
4798
|
+
filled_data.media_spend,
|
|
4714
4799
|
list(self._meridian.input_data.media_channel.values),
|
|
4715
4800
|
)
|
|
4716
4801
|
|
|
@@ -4723,18 +4808,16 @@ class Analyzer:
|
|
|
4723
4808
|
or self._meridian.rf_tensors.rf_spend is None
|
|
4724
4809
|
):
|
|
4725
4810
|
warnings.warn(
|
|
4726
|
-
"Requested spends for paid media channels with R&F data, but
|
|
4811
|
+
"Requested spends for paid media channels with R&F data, but the"
|
|
4727
4812
|
" channels are not available.",
|
|
4728
4813
|
)
|
|
4729
4814
|
aggregated_rf_spend = empty_da
|
|
4730
4815
|
else:
|
|
4731
|
-
rf_execution_values =
|
|
4732
|
-
self._meridian.rf_tensors.reach * self._meridian.rf_tensors.frequency
|
|
4733
|
-
)
|
|
4816
|
+
rf_execution_values = filled_data.reach * filled_data.frequency
|
|
4734
4817
|
aggregated_rf_spend = self._impute_and_aggregate_spend(
|
|
4735
4818
|
selected_times,
|
|
4736
4819
|
rf_execution_values,
|
|
4737
|
-
|
|
4820
|
+
filled_data.rf_spend,
|
|
4738
4821
|
list(self._meridian.input_data.rf_channel.values),
|
|
4739
4822
|
)
|
|
4740
4823
|
|
|
@@ -4744,7 +4827,7 @@ class Analyzer:
|
|
|
4744
4827
|
|
|
4745
4828
|
def _impute_and_aggregate_spend(
|
|
4746
4829
|
self,
|
|
4747
|
-
selected_times: Sequence[str] | None,
|
|
4830
|
+
selected_times: Sequence[str] | Sequence[bool] | None,
|
|
4748
4831
|
media_execution_values: tf.Tensor,
|
|
4749
4832
|
channel_spend: tf.Tensor,
|
|
4750
4833
|
channel_names: Sequence[str],
|
|
@@ -4759,7 +4842,7 @@ class Analyzer:
|
|
|
4759
4842
|
argument, its values only affect the output when imputation is required.
|
|
4760
4843
|
|
|
4761
4844
|
Args:
|
|
4762
|
-
selected_times: The time period to get the
|
|
4845
|
+
selected_times: The time period to get the aggregated spend.
|
|
4763
4846
|
media_execution_values: The media execution values over all time points.
|
|
4764
4847
|
channel_spend: The spend over all time points. Its shape can be `(n_geos,
|
|
4765
4848
|
n_times, n_media_channels)` or `(n_media_channels,)` if the data is
|
|
@@ -4775,17 +4858,24 @@ class Analyzer:
|
|
|
4775
4858
|
"selected_times": selected_times,
|
|
4776
4859
|
"aggregate_geos": True,
|
|
4777
4860
|
"aggregate_times": True,
|
|
4861
|
+
"flexible_time_dim": True,
|
|
4778
4862
|
}
|
|
4779
4863
|
|
|
4780
4864
|
if channel_spend.ndim == 3:
|
|
4781
4865
|
aggregated_spend = self.filter_and_aggregate_geos_and_times(
|
|
4782
4866
|
channel_spend,
|
|
4867
|
+
has_media_dim=True,
|
|
4783
4868
|
**dim_kwargs,
|
|
4784
4869
|
).numpy()
|
|
4785
4870
|
# channel_spend.ndim can only be 3 or 1.
|
|
4786
4871
|
else:
|
|
4787
4872
|
# media spend can have more time points than the model time points
|
|
4788
|
-
|
|
4873
|
+
if media_execution_values.shape[1] == self._meridian.n_media_times:
|
|
4874
|
+
media_exe_values = media_execution_values[
|
|
4875
|
+
:, -self._meridian.n_times :, :
|
|
4876
|
+
]
|
|
4877
|
+
else:
|
|
4878
|
+
media_exe_values = media_execution_values
|
|
4789
4879
|
# Calculates CPM over all times and geos if the spend does not have time
|
|
4790
4880
|
# and geo dimensions.
|
|
4791
4881
|
target_media_exe_values = self.filter_and_aggregate_geos_and_times(
|