Count an event once in a KPI, when several sources report it - #2472
Count an event once in a KPI, when several sources report it#2472Flix6x wants to merge 4 commits into
Conversation
Two sources reporting one event are two claims about it, not two contributions to it, so adding them up produced a number no source ever reported, and that no point on the chart showed. A sensor with a forecast later corrected by an upload, or with two forecasters configured differently, totalled both. The KPI query now asks for one deterministic belief per event, which prefers the latest source version, and the most recent belief within it. `most_recent_beliefs_only` alone did not do this: it is per source, and `use_latest_version_per_event` only collapses sources sharing a name, type and model, and only within one belief time. The chart beside the KPI still draws every source, so it can show more points than the KPI counted, which the KPI documentation now says. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lp1bUhWjEQtyDbnvRZQgQs Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lp1bUhWjEQtyDbnvRZQgQs Signed-off-by: F.N. Claessen <claessen@seita.nl>
Documentation build overview
|
There was a problem hiding this comment.
🟡 Changes recommended
There are determinism and coverage gaps (tie-breaking behavior and a test/doc mismatch around “latest source version”) that should be addressed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adjusts KPI calculation on the asset page so that when multiple data sources report the same event, the KPI reduces one deterministic value per event (instead of summing multiple “claims” into a misleading total). This aligns KPI semantics with “one value per event” while keeping the chart behavior (still showing all sources) explicitly documented.
Changes:
- Update the KPI query to request
one_deterministic_belief_per_event=True, so the KPI reduces a single value per event. - Add a regression test covering the “two sources, same event” scenario.
- Document the new KPI-vs-chart behavior and add a bugfix changelog entry.
File summaries
| File | Description |
|---|---|
flexmeasures/api/v3_0/assets.py |
Changes KPI belief search to select a single deterministic belief per event |
flexmeasures/api/v3_0/tests/test_assets_api.py |
Adds a regression test for multi-source reporting of the same event |
documentation/views/asset-data.rst |
Documents that KPIs reduce one value per event and may differ from chart point counts |
documentation/changelog.rst |
Adds a bugfix entry describing the corrected KPI behavior |
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| beliefs = sensor.search_beliefs( | ||
| event_starts_after=start, | ||
| event_ends_before=end, | ||
| most_recent_beliefs_only=True, | ||
| one_deterministic_belief_per_event=True, | ||
| ) |
There was a problem hiding this comment.
Good catch on the ambiguity, but the fix you suggest would break a documented feature — so I have
left the tie alone and documented why.
I did implement it first: a last tie-break by highest source id, matching source_priorities, which
ranks by version and then by id. Two existing tests then failed, and the second one is the answer:
test_source_transition (flexmeasures/data/models/reporting/tests/test_aggregator.py) states
In case of encountering more than one source per event, the first source defined in the sources
array is prioritized.
An AggregatorReporter given sources=[ds1, ds2] expects ds1 to win the events both report. That
works because the remaining tie keeps the order the rows arrive in. Breaking the tie by source id
made ds2 win, since it has the higher id, and the reporter silently stopped honouring the caller's
order. test_select_latest_version_and_belief_per_event_equivalence failed too, since its reference
implementation keeps the incumbent on an exact tie.
So the order is not arbitrary noise to be tidied away — it is how precedence is expressed. What was
missing is that this is nowhere written down, so _select_latest_version_and_belief_per_event now
says it:
Beliefs that tie on both keep the order they came in, which is what lets a caller express its own
precedence by the order in which it passes its sources.
For the KPI endpoint, which passes no sources of its own, an exact tie — same version, same belief
time, two sources — does leave the value up to the order the rows come back in. That is a real
limit, and a narrow one: it needs two sources to claim one event at the very same moment under the
same version. Worth its own issue if we want KPIs to be deterministic even then; it is not something
this PR can settle without taking the reporter's precedence away.
| sources = list(setup_sources.values()) | ||
| reported, corrected = sources[0], sources[-1] | ||
| assert reported.id != corrected.id, "this test needs two distinct sources" | ||
|
|
There was a problem hiding this comment.
Right — setup_sources creates sources without versions, so that test only ever showed the belief
time. Two changes in 097070f.
The docstring now says what the test does: these two sources are of the same version, so the one
that believed the event more recently is the one the KPI counts.
And a new test covers the version half, with two versions of one reporter, the newer one speaking
first:
# The newer version spoke first, and the older version spoke later.
...
assert total == pytest.approx(42.0), "the newer version's value, despite the older belief time"It reports 141.0 without the fix — 42 plus 99 — so it covers both the summing and the ordering.
The regression test only varied the belief time, while the docstring and the documentation also claimed a preference for the latest source version, which the fixture's unversioned sources could not show. A second test now gives one reporter two versions and has the newer one speak first, so the KPI answers with the newer version's value despite the older belief time. Ties beyond that are left alone, and `_select_latest_version_and_belief_per_event` now says why: beliefs which tie on version and belief time keep the order they came in, which is how a caller expresses its own precedence by the order it passes its sources. `test_source_transition` documents and relies on that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LtMZ49GH6LaiY5MdgtfNe2 Signed-off-by: F.N. Claessen <claessen@seita.nl>
There was a problem hiding this comment.
🟡 Changes recommended
A newly added docstring violates the repository’s “break lines only after punctuation” convention, and should be reformatted before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Lite
| Beliefs that tie on both keep the order they came in, which is what lets a caller | ||
| express its own precedence by the order in which it passes its sources. | ||
| See `test_source_transition`, where the first source in the list wins the events both sources report. |
There was a problem hiding this comment.
Reflowed in da9ebb9 — the break now falls after the comma, so both lines end at punctuation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LtMZ49GH6LaiY5MdgtfNe2 Signed-off-by: F.N. Claessen <claessen@seita.nl>
There was a problem hiding this comment.
🟢 Approval recommended
The change is small and well-targeted, with clear documentation updates and new tests covering the previously incorrect KPI behavior and its selection rules.
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 0 new
- Review effort level: Lite
Description
Two data sources reporting the same event are two claims about it, not two contributions to it —
but a KPI added them up. The total then showed a number no source ever reported, and that no point on
the chart beside it showed either.
One sensor, one day, reported by two sources:
A
sumKPI over that day reported 180.0. It now reports 80.0, the more recent belief aboutthe event.
This is direction 1 from #2471, where the alternatives are written up.
Why
most_recent_beliefs_onlywas not enoughThat filter is per source, so it leaves one row per event per source. The default
use_latest_version_per_event=Truedoes collapse sources that share a(name, type, model), butonly within one belief time, so two sources believing the same event at different moments both
survive into the values the KPI reduces.
The KPI query now also asks for
one_deterministic_belief_per_event=True, which picks one value perevent: latest source version first, most recent belief within that second.
What changes for users
A KPI over a sensor that only ever has one source per event is unaffected — which is the common case,
and every existing KPI test but the new one covers it.
Where a sensor does carry several sources for one event, the KPI now reports one of their values
instead of their sum. The chart still draws every source, so it can show more points than the KPI
counted. The KPI documentation says so now, since the two no longer agree point for point in that
case — the previous behaviour agreed with the chart by summing what it drew, which is exactly what
produced the misleading total.
documentation/changelog.rstHow to test
test_kpi_counts_an_event_once_when_two_sources_report_itis the example above. It fails onmainwith
assert 180.0 == 80.0 ± 8.0e-05.test_kpi_reports_what_the_chart_drawsstill passes unchanged: its two sources report differentevents, so summing across them was always the right answer, and still is.
Related items
Closes #2471. Found while reviewing #2464, which makes a scheduled sensor able to carry schedules from
several sources and so widens the window in which this bites, but the behaviour predates it and
reproduces on
main.Sign-off
🤖 Generated with Claude Code
https://claude.ai/code/session_01Lp1bUhWjEQtyDbnvRZQgQs