Make AnalyzedSeries change points lazy properties - #169
Conversation
There was a problem hiding this comment.
Pull request overview
Defers AnalyzedSeries change-point computation until first access and caches the results.
Changes:
- Adds lazy, read-only change-point properties.
- Updates append/deserialization to populate private caches directly.
- Adds regression tests for laziness, caching, append, and deserialization.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
otava/series.py |
Implements lazy change-point computation and caching. |
tests/series_test.py |
Adds and updates lazy-computation tests. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| self.__weak_change_points = w | ||
| self.__change_points_by_time = self.change_points.by_time() |
| analyzed = test.analyze() | ||
| assert calls["count"] == 0 |
Gerrrr
left a comment
There was a problem hiding this comment.
This is a very nice optimization, thank you for opening this PR! I left a few suggestions + please rebase against latest master.
| """ | ||
| Time series data with computed change points. | ||
|
|
||
| Change points are computed lazily, on first access. Constructing an |
There was a problem hiding this comment.
nit: I don't think we need this addition to the doc.
| ) | ||
|
|
||
| analyzed = test.analyze() | ||
| analyzed.append(time=[len(data)], new_data={"m1": [5.0]}, attributes={}) |
There was a problem hiding this comment.
This only covers a series with an existing change point. A stable series computes an empty result, which _validate_append() mistakes for “not computed.” Could you please add a case that does not produce a change point and explicitly ensure computation rather than checking collection truthiness?
|
|
||
| def __ensure_change_points_computed(self): | ||
| if self.__change_points is None: | ||
| cp, weak_cps = self.__compute_change_points(self.__series, self.options) |
There was a problem hiding this comment.
logging.info("Computing change points... from line 128 should move here.
debfc8f to
3fb2201
Compare
|
Done, and rebased onto a1ef00d.
The stable-series test then surfaced a second bug: 231 non-container tests pass. |
3fb2201 to
9168070
Compare
Constructing an
AnalyzedSeriesruns change point detection for every metric up front, even when the caller never reads the results. #79 asks forchange_pointsandchange_points_by_timeto become properties instead.change_points,weak_change_points,change_points_by_timeandchange_points_timestampare now read-only properties backed by private fields. The first access computes everything once and caches it;append()andfrom_json()write the backing fields directly. Two behavior notes:change_points_timestampnow records when the change points were actually computed rather than when the object was constructed. A timestamp stored in JSON still takes precedence on deserialization.change_pointsargument used to leaveweak_change_pointsunset, so reading it raisedAttributeErrorunlessfrom_jsonpatched it afterwards. It now defaults to an empty collection.Cost measured before (master, cd5bc1e) and after (this branch) with the same script on the same machine (Linux, Xeon @ 2.8GHz, 8 GB RAM, Python 3.11), seeded random data. Every number is the median of 11 runs and is copied verbatim from the millisecond output of the script below; six decimals in milliseconds is exact nanosecond resolution, so nothing is rounded away, and the script prints the raw nanosecond medians alongside:
The cost moves from the constructor to the first read; the total for callers that do read change points stays in the same range, and repeated reads stay cached. Detection results are unchanged: no expected value in the test suite needed to change. The only adjusted test is
test_validate, which used to fake an uncomputed state by assigningchange_points = Noneand now constructs with an explicitly empty collection.Benchmark script
Verification:
pytest tests perf(non-container): 194 passed, including three new regression tests for laziness/caching,append()on a freshly constructed instance, and deserialization without recomputationflake8,ruff check,ruff format --check,isort --check-only: cleangit diff --check: cleanCloses #79