Files
comunidadhll/docs/HISTORICAL_MATCH_KILLS_PER_MINUTE_ANALYSIS.md

5.9 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:
    • kills
    • deaths
    • teamkills
    • weapon and matchup counters
  • per match:
    • started_at
    • ended_at
    • duration_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:

  • connected
  • disconnected
  • match_start
  • match_end

Supporting evidence still stored in the player fact:

  • first_seen_server_time
  • last_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 NULL
  • active_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
  • if the player disconnects during the match:
    • close interval at disconnected.server_time
  • if the player was already connected before match_start and there is no later pre-match disconnect:
    • open interval at match_start
  • if the player is still connected at match_end:
    • close interval at match_end
  • 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_seconds exists
  • player_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_seconds
  • player_active_minutes
  • kpm
  • kpm_status
  • active_time_source

active_time_source values currently used:

  • connection_intervals
  • connection_intervals_carryover
  • event_span_fallback
  • unavailable

kpm_status values:

  • ready
  • missing_active_time
  • insufficient_active_time
  • missing_connection_intervals

Rules:

  • missing active time:
    • kpm = null
    • kpm_status = "missing_active_time"
  • fallback event span without reliable connection intervals:
    • kpm = null
    • kpm_status = "missing_connection_intervals"
  • active time below threshold:
    • kpm = null
    • kpm_status = "insufficient_active_time"
  • valid active time:
    • kpm = round(kills / (player_active_seconds / 60), 2)
    • kpm_status = "ready"
    • only when active_time_source is connection_intervals or connection_intervals_carryover

Historical Matches Already Stored

Old matches are not backfilled with fake KPM.

For those rows:

  • player_active_seconds can remain null
  • kpm stays null
  • frontend must not render 0.00 as 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

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_match means 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_intervals
  • active_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/partida for kills_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.