7.0 KiB
Historical Match Kills Per Minute Analysis
Scope
This document defines how real historical KPM works for the match detail served to historico-partida.html.
Why KPM Was Not Safe Before
Historical match detail already exposed:
- per player:
killsdeathsteamkills- weapon and matchup counters
- per match:
started_atended_atduration_seconds
What it did not expose was player-level active time. Because of that, dividing by total match duration would have produced a false player KPM.
Real KPM Rule
Real KPM means:
kills / (player_active_seconds / 60)
It does not mean:
kills / match_duration_minutes
Source of Truth
The current implementation uses connection intervals reconstructed from the materialized RCON AdminLog match model.
Reliable interval signals:
connecteddisconnectedmatch_startmatch_end
Supporting evidence still stored in the player fact:
first_seen_server_timelast_seen_server_time
The persisted field is:
player_active_seconds
The source label is:
active_time_source = "event_log"
Persistence
Forward-only persistence now stores on rcon_match_player_stats:
player_active_seconds INTEGER NULLactive_time_source TEXT
Legacy rows remain valid. If they were materialized before these columns existed, they keep player_active_seconds = NULL until new materialization or new matches populate the field.
Calculation
Observed active time is now:
sum(connected_interval_seconds clamped to [match_start, match_end])
Rules:
- if the player connects during the match:
- open interval at
connected.server_time
- open interval at
- if the player disconnects during the match:
- close interval at
disconnected.server_time
- close interval at
- if the player was already connected before
match_startand there is no later pre-match disconnect:- open interval at
match_start
- open interval at
- if the player is still connected at
match_end:- close interval at
match_end
- close interval at
- if the player reconnects multiple times:
- sum all non-overlapping intervals
This is still observed presence, not exact telemetry down to every silent second.
KPM is exposed only when:
player_active_secondsexistsplayer_active_seconds >= HLL_KPM_MIN_ACTIVE_SECONDS
Default:
HLL_KPM_MIN_ACTIVE_SECONDS = 60
Payload Contract
Historical match detail player rows may now expose:
player_active_secondsplayer_active_minuteskpmkpm_statusactive_time_source
active_time_source values currently used:
connection_intervalsconnection_intervals_carryoverevent_span_fallbackunavailable
kpm_status values:
readymissing_active_timeinsufficient_active_timemissing_connection_intervals
Rules:
- missing active time:
kpm = nullkpm_status = "missing_active_time"
- fallback event span without reliable connection intervals:
kpm = nullkpm_status = "missing_connection_intervals"
- active time below threshold:
kpm = nullkpm_status = "insufficient_active_time"
- valid active time:
kpm = round(kills / (player_active_seconds / 60), 2)kpm_status = "ready"- only when
active_time_sourceisconnection_intervalsorconnection_intervals_carryover
Historical Matches Already Stored
Old matches are not backfilled with fake KPM.
For those rows:
player_active_secondscan remainnullkpmstaysnull- frontend must not render
0.00as if the value were real - rows rematerialized without reliable connection intervals can still expose
event_span_fallback, but that fallback must not be shown as real KPM
Frontend Rule
historico-partida.js renders KPM only when:
kpm_status == "ready"
If the value is missing or below threshold, the panel stays clean and does not show a fake metric.
Public Tables
The historical match detail now exposes the same real KPM in two public places:
- expanded player panel in
historico-partida.html - main players table in
historico-partida.html
The main table leaves the KPM cell empty when kpm_status != "ready".
This avoids false 0.00 values for:
- legacy rows without active time
- rows below
HLL_KPM_MIN_ACTIVE_SECONDS - rows that only expose
event_span_fallback
Personal Player Profile
The public player profile in stats.html can now expose real KPM for the selected weekly or monthly window when the profile endpoint can aggregate:
SUM(player_active_seconds)- only over rows with:
active_time_source = connection_intervalsactive_time_source = connection_intervals_carryoverplayer_active_seconds >= HLL_KPM_MIN_ACTIVE_SECONDS
Weekly and monthly profile KPM use:
sum(eligible_kills) / (sum(eligible_player_active_seconds) / 60)
This is intentionally narrower than the window total:
- profile
killsstill show all kills in the selected window - profile
KPMonly becomesreadywhen the active-time subset is reliable enough
The profile payload now exposes:
player_active_secondsplayer_active_minuteskpmkpm_statusactive_time_sourceactive_time_coverage
If the profile read model is empty, the endpoint can safely fall back to runtime aggregation over rcon_match_player_stats for that single player and time window. This avoids inventing KPM while keeping the page usable before the read model is refreshed.
Kills Per Match Labels
Several public tables were showing kills_per_match under the visible label KPM.
That is no longer acceptable because:
kills_per_matchmeans kills divided by matches considered- real KPM means kills divided by active minutes from reliable connection intervals
Public surfaces that now use Kills/partida instead of KPM for kills_per_match:
- historical weekly and monthly leaderboard tables
- annual stats summary table
- annual stats comparison cards
- ranking metric selector and ranking table label
Aggregated Real KPM
This repository does not yet expose a public weekly, monthly or annual aggregate KPM based only on:
active_time_source = connection_intervalsactive_time_source = connection_intervals_carryover
That was intentionally left out of this change because the public leaderboard and ranking snapshots would need an explicit coverage contract before mixing:
- rows with real active time
- older rows without it
- fallback rows blocked from
kpm_status = ready
Until that contract exists, public aggregated views keep:
Kills/partidaforkills_per_match- real KPM only at historical match detail player level
Limitations
- This is observed active time from AdminLog connection evidence, not exact join/leave telemetry for every silent second.
- Quiet players with kills/chat/team switches but no reliable connection chain can expose
event_span_fallback; that span is intentionally blocked from KPM. - Legacy matches remain without KPM unless rematerialized from stored AdminLog evidence.
- We do not discount time spent without squad/unit/role yet because there is no audited historical source for that dimension in this implementation.