google / google/meridian

meridian_serde.save_meridian() fails on non-uniform (e.g. calendar-monthly) time axes that InputData validation accepts

Open
#1,675 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
1.5k
Forks
294
Avg merge
1d 21h
Merged PRs (30d)
47

Description

Meridian version: 1.7.0 (also happens on older versions)
Python: 3.12

### What happens

Calling `save_meridian` on a model with calendar-monthly time coordinates (1st of each month) raises:

```
ValueError: Interval length between selected times must be consistent.
```

The odd part is that the same model passes Meridian's own `InputData` validation and trains and analyses fine. Only saving to `.binpb` fails. So the input layer treats monthly data as valid but the serializer does not.

### Reproduce

```python
from meridian.schema.serde import meridian_serde

# mmm has monthly time coords: 2024-01-01, 2024-02-01, 2024-03-01, ...
# day gaps are 31, 29, 31, 30, ...
meridian_serde.save_meridian(mmm, "model.binpb")
```

Traceback (trimmed):

```
meridian/schema/serde/marketing_data.py in __call__(self)
--> times_to_date_intervals = time_record.convert_times_to_date_intervals(
self._input_data.media_time.data)
meridian/schema/utils/time_record.py in convert_times_to_date_intervals(times)
--> raise ValueError("Interval length between selected times must be consistent.")
```

### What I found

The error comes from `convert_times_to_date_intervals` in `meridian/schema/utils/time_record.py`. It takes the gap between the first two dates as the expected interval, then requires every following gap to match it exactly:
```python
interval_length = _compute_interval_length(datetimes[0], datetimes[1]) for i, start_date in enumerate(datetimes):
...
if current_interval_length != interval_length: raise ValueError("Interval length between selected times must be consistent.")
```

Calendar months are 28/29/30/31 days, so the second gap already differs and it raises.

Meanwhile `_is_regular_time_index` in `meridian/data/time_coordinates.py` (used by `InputData._validate_times`) explicitly allows monthly (28 to 31), quarterly (90 to 92) and yearly (365 to 366) spacing. the two checks disagree.

As far as I can tell the format itself does not need uniform spacing. The serializer writes an explicit `DateInterval` (start, end) onto every data point from the actual dates, and deserialize reads each time back from its stored start date. The only place a single interval is actually neede of the last period, which has no next date to look at.

### Possible fix

Drop the strict check and derive the last period's end from the final gap instead of the first one:

```python
final_interval_length = _compute_interval_length(datetimes[-2], datetimes[-1])
for i, start_date in enumerate(datetimes):
end_date = (start_date + timedelta(days=final_interval_length)
if i == len(datetimes) - 1 else datetimes[i + 1])
...
```

That would accept any increasing time axis, which lines up with what `InputData` alreadyhe mean interval (`interval_days`) which already tolerates monthly, so it should be fine.

### Question

Is monthly data meant to be supported here, or is the uniform interval requirement intentional for `.binpb`? If it is supposed to work, is this something that could be fixed? Happy to help or put up a PR if that would be useful.

Contributor guide

Open the contributing guide

Research direction

Start in meridian/schema/utils/time_record.py at convert_times_to_date_intervals, then compare its assumptions with _is_regular_time_index in meridian/data/time_coordinates.py. Reproduce saving a calendar-monthly model, adjust the interval handling so accepted monthly axes serialize, and verify that the resulting .binpb data can be deserialized with the original dates.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend, data
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.