diff --git a/ETL/L3_Builder.py b/ETL/L3_Builder.py index 9f6a705..3071f0e 100644 --- a/ETL/L3_Builder.py +++ b/ETL/L3_Builder.py @@ -48,6 +48,18 @@ def init_db(): "rd_phase_death_early_share": "REAL", "rd_phase_death_mid_share": "REAL", "rd_phase_death_late_share": "REAL", + "rd_phase_kill_early_share_t": "REAL", + "rd_phase_kill_mid_share_t": "REAL", + "rd_phase_kill_late_share_t": "REAL", + "rd_phase_kill_early_share_ct": "REAL", + "rd_phase_kill_mid_share_ct": "REAL", + "rd_phase_kill_late_share_ct": "REAL", + "rd_phase_death_early_share_t": "REAL", + "rd_phase_death_mid_share_t": "REAL", + "rd_phase_death_late_share_t": "REAL", + "rd_phase_death_early_share_ct": "REAL", + "rd_phase_death_mid_share_ct": "REAL", + "rd_phase_death_late_share_ct": "REAL", "rd_firstdeath_team_first_death_rounds": "INTEGER", "rd_firstdeath_team_first_death_win_rate": "REAL", "rd_invalid_death_rounds": "INTEGER", diff --git a/database/L1A/L1A.sqlite b/database/L1A/L1A.sqlite index 0f441f4..0b1e98f 100644 Binary files a/database/L1A/L1A.sqlite and b/database/L1A/L1A.sqlite differ diff --git a/database/L2/L2_Main.sqlite b/database/L2/L2_Main.sqlite index e56ee38..f157555 100644 Binary files a/database/L2/L2_Main.sqlite and b/database/L2/L2_Main.sqlite differ diff --git a/database/L3/L3_Features.sqlite b/database/L3/L3_Features.sqlite index 3789a43..bea49ca 100644 Binary files a/database/L3/L3_Features.sqlite and b/database/L3/L3_Features.sqlite differ diff --git a/database/L3/schema.sql b/database/L3/schema.sql index d35db0d..588389e 100644 --- a/database/L3/schema.sql +++ b/database/L3/schema.sql @@ -204,6 +204,18 @@ CREATE TABLE IF NOT EXISTS dm_player_features ( rd_phase_death_early_share REAL, rd_phase_death_mid_share REAL, rd_phase_death_late_share REAL, + rd_phase_kill_early_share_t REAL, + rd_phase_kill_mid_share_t REAL, + rd_phase_kill_late_share_t REAL, + rd_phase_kill_early_share_ct REAL, + rd_phase_kill_mid_share_ct REAL, + rd_phase_kill_late_share_ct REAL, + rd_phase_death_early_share_t REAL, + rd_phase_death_mid_share_t REAL, + rd_phase_death_late_share_t REAL, + rd_phase_death_early_share_ct REAL, + rd_phase_death_mid_share_ct REAL, + rd_phase_death_late_share_ct REAL, rd_firstdeath_team_first_death_rounds INTEGER, rd_firstdeath_team_first_death_win_rate REAL, rd_invalid_death_rounds INTEGER, diff --git a/database/Web/Web_App.sqlite b/database/Web/Web_App.sqlite index 1d393c6..5695a31 100644 Binary files a/database/Web/Web_App.sqlite and b/database/Web/Web_App.sqlite differ diff --git a/web/services/feature_service.py b/web/services/feature_service.py index a49540f..a052b71 100644 --- a/web/services/feature_service.py +++ b/web/services/feature_service.py @@ -1202,6 +1202,18 @@ class FeatureService: df['rd_phase_death_early_share'] = 0.0 df['rd_phase_death_mid_share'] = 0.0 df['rd_phase_death_late_share'] = 0.0 + df['rd_phase_kill_early_share_t'] = 0.0 + df['rd_phase_kill_mid_share_t'] = 0.0 + df['rd_phase_kill_late_share_t'] = 0.0 + df['rd_phase_kill_early_share_ct'] = 0.0 + df['rd_phase_kill_mid_share_ct'] = 0.0 + df['rd_phase_kill_late_share_ct'] = 0.0 + df['rd_phase_death_early_share_t'] = 0.0 + df['rd_phase_death_mid_share_t'] = 0.0 + df['rd_phase_death_late_share_t'] = 0.0 + df['rd_phase_death_early_share_ct'] = 0.0 + df['rd_phase_death_mid_share_ct'] = 0.0 + df['rd_phase_death_late_share_ct'] = 0.0 df['rd_firstdeath_team_first_death_rounds'] = 0 df['rd_firstdeath_team_first_death_win_rate'] = 0.0 df['rd_invalid_death_rounds'] = 0 @@ -1268,6 +1280,62 @@ class FeatureService: df[c] = df[f'{c}_calc'].fillna(df[c]) df.drop(columns=[f'{c}_calc'], inplace=True) + if 'attacker_side' in df_events.columns: + k_side = df_events[df_events['attacker_side'].isin(['CT', 'T'])].copy() + if not k_side.empty: + k_cnt_side = k_side.groupby(['attacker_steam_id', 'attacker_side', 'phase_bucket']).size().reset_index(name='cnt') + k_piv = k_cnt_side.pivot_table(index=['attacker_steam_id', 'attacker_side'], columns='phase_bucket', values='cnt', fill_value=0) + k_piv['tot'] = k_piv.sum(axis=1).replace(0, 1) + k_piv = k_piv.div(k_piv['tot'], axis=0).drop(columns=['tot']) + k_piv = k_piv.reset_index().rename(columns={'attacker_steam_id': 'steam_id_64'}) + + for side, suffix in [('T', '_t'), ('CT', '_ct')]: + tmp = k_piv[k_piv['attacker_side'] == side].copy() + if not tmp.empty: + tmp = tmp.rename(columns={ + 'early': f'rd_phase_kill_early_share{suffix}', + 'mid': f'rd_phase_kill_mid_share{suffix}', + 'late': f'rd_phase_kill_late_share{suffix}', + }) + df = df.merge( + tmp[['steam_id_64', f'rd_phase_kill_early_share{suffix}', f'rd_phase_kill_mid_share{suffix}', f'rd_phase_kill_late_share{suffix}']], + on='steam_id_64', + how='left', + suffixes=('', '_calc') + ) + for c in [f'rd_phase_kill_early_share{suffix}', f'rd_phase_kill_mid_share{suffix}', f'rd_phase_kill_late_share{suffix}']: + if f'{c}_calc' in df.columns: + df[c] = df[f'{c}_calc'].fillna(df[c]) + df.drop(columns=[f'{c}_calc'], inplace=True) + + if 'victim_side' in df_events.columns: + d_side = df_events[df_events['victim_side'].isin(['CT', 'T'])].copy() + if not d_side.empty: + d_cnt_side = d_side.groupby(['victim_steam_id', 'victim_side', 'phase_bucket']).size().reset_index(name='cnt') + d_piv = d_cnt_side.pivot_table(index=['victim_steam_id', 'victim_side'], columns='phase_bucket', values='cnt', fill_value=0) + d_piv['tot'] = d_piv.sum(axis=1).replace(0, 1) + d_piv = d_piv.div(d_piv['tot'], axis=0).drop(columns=['tot']) + d_piv = d_piv.reset_index().rename(columns={'victim_steam_id': 'steam_id_64'}) + + for side, suffix in [('T', '_t'), ('CT', '_ct')]: + tmp = d_piv[d_piv['victim_side'] == side].copy() + if not tmp.empty: + tmp = tmp.rename(columns={ + 'early': f'rd_phase_death_early_share{suffix}', + 'mid': f'rd_phase_death_mid_share{suffix}', + 'late': f'rd_phase_death_late_share{suffix}', + }) + df = df.merge( + tmp[['steam_id_64', f'rd_phase_death_early_share{suffix}', f'rd_phase_death_mid_share{suffix}', f'rd_phase_death_late_share{suffix}']], + on='steam_id_64', + how='left', + suffixes=('', '_calc') + ) + for c in [f'rd_phase_death_early_share{suffix}', f'rd_phase_death_mid_share{suffix}', f'rd_phase_death_late_share{suffix}']: + if f'{c}_calc' in df.columns: + df[c] = df[f'{c}_calc'].fillna(df[c]) + df.drop(columns=[f'{c}_calc'], inplace=True) + if 'victim_side' in df_events.columns and 'winner_side' in df_events.columns: death_rows = df_events[['match_id', 'round_num', 'event_time', 'victim_steam_id', 'victim_side', 'winner_side']].copy() death_rows = death_rows[death_rows['victim_side'].isin(['CT', 'T']) & death_rows['winner_side'].isin(['CT', 'T'])] diff --git a/web/services/stats_service.py b/web/services/stats_service.py index de17852..ee1bb72 100644 --- a/web/services/stats_service.py +++ b/web/services/stats_service.py @@ -749,6 +749,10 @@ class StatsService: # New: ROUND (Round Dynamics) 'rd_phase_kill_early_share', 'rd_phase_kill_mid_share', 'rd_phase_kill_late_share', 'rd_phase_death_early_share', 'rd_phase_death_mid_share', 'rd_phase_death_late_share', + 'rd_phase_kill_early_share_t', 'rd_phase_kill_mid_share_t', 'rd_phase_kill_late_share_t', + 'rd_phase_kill_early_share_ct', 'rd_phase_kill_mid_share_ct', 'rd_phase_kill_late_share_ct', + 'rd_phase_death_early_share_t', 'rd_phase_death_mid_share_t', 'rd_phase_death_late_share_t', + 'rd_phase_death_early_share_ct', 'rd_phase_death_mid_share_ct', 'rd_phase_death_late_share_ct', 'rd_firstdeath_team_first_death_win_rate', 'rd_invalid_death_rate', 'rd_pressure_kpr_ratio', 'rd_matchpoint_kpr_ratio', 'rd_trade_response_10s_rate', 'rd_pressure_perf_ratio', 'rd_matchpoint_perf_ratio', diff --git a/web/templates/players/profile.html b/web/templates/players/profile.html index d917c0d..6a9fd70 100644 --- a/web/templates/players/profile.html +++ b/web/templates/players/profile.html @@ -349,8 +349,99 @@
Phase Split
-
- + {% macro phase_row(title, ke, km, kl, de, dm, dl, ke_key, km_key, kl_key, de_key, dm_key, dl_key) %} + {% set ke = ke or 0 %} + {% set km = km or 0 %} + {% set kl = kl or 0 %} + {% set de = de or 0 %} + {% set dm = dm or 0 %} + {% set dl = dl or 0 %} + {% set k_total = ke + km + kl %} + {% set d_total = de + dm + dl %} +
+
{{ title }}
+
+
+ {% if k_total > 0 %} +
+
+
+ {% else %} +
+ {% endif %} +
+
+ + E {{ '{:.0%}'.format(ke) }} + {% if distribution and distribution.get(ke_key) %} (#{{ distribution.get(ke_key).rank }}/{{ distribution.get(ke_key).total }}){% endif %} + + + M {{ '{:.0%}'.format(km) }} + {% if distribution and distribution.get(km_key) %} (#{{ distribution.get(km_key).rank }}/{{ distribution.get(km_key).total }}){% endif %} + + + L {{ '{:.0%}'.format(kl) }} + {% if distribution and distribution.get(kl_key) %} (#{{ distribution.get(kl_key).rank }}/{{ distribution.get(kl_key).total }}){% endif %} + +
+
+
+
+ {% if d_total > 0 %} +
+
+
+ {% else %} +
+ {% endif %} +
+
+ + E {{ '{:.0%}'.format(de) }} + {% if distribution and distribution.get(de_key) %} (#{{ distribution.get(de_key).rank }}/{{ distribution.get(de_key).total }}){% endif %} + + + M {{ '{:.0%}'.format(dm) }} + {% if distribution and distribution.get(dm_key) %} (#{{ distribution.get(dm_key).rank }}/{{ distribution.get(dm_key).total }}){% endif %} + + + L {{ '{:.0%}'.format(dl) }} + {% if distribution and distribution.get(dl_key) %} (#{{ distribution.get(dl_key).rank }}/{{ distribution.get(dl_key).total }}){% endif %} + +
+
+
+ {% endmacro %} + +
+
+
+ KillsE / M / L +
+
+ DeathsE / M / L +
+
+ +
+ {{ phase_row('Total', + features.get('rd_phase_kill_early_share', 0), features.get('rd_phase_kill_mid_share', 0), features.get('rd_phase_kill_late_share', 0), + features.get('rd_phase_death_early_share', 0), features.get('rd_phase_death_mid_share', 0), features.get('rd_phase_death_late_share', 0), + 'rd_phase_kill_early_share', 'rd_phase_kill_mid_share', 'rd_phase_kill_late_share', + 'rd_phase_death_early_share', 'rd_phase_death_mid_share', 'rd_phase_death_late_share' + ) }} + {{ phase_row('T', + features.get('rd_phase_kill_early_share_t', 0), features.get('rd_phase_kill_mid_share_t', 0), features.get('rd_phase_kill_late_share_t', 0), + features.get('rd_phase_death_early_share_t', 0), features.get('rd_phase_death_mid_share_t', 0), features.get('rd_phase_death_late_share_t', 0), + 'rd_phase_kill_early_share_t', 'rd_phase_kill_mid_share_t', 'rd_phase_kill_late_share_t', + 'rd_phase_death_early_share_t', 'rd_phase_death_mid_share_t', 'rd_phase_death_late_share_t' + ) }} + {{ phase_row('CT', + features.get('rd_phase_kill_early_share_ct', 0), features.get('rd_phase_kill_mid_share_ct', 0), features.get('rd_phase_kill_late_share_ct', 0), + features.get('rd_phase_death_early_share_ct', 0), features.get('rd_phase_death_mid_share_ct', 0), features.get('rd_phase_death_late_share_ct', 0), + 'rd_phase_kill_early_share_ct', 'rd_phase_kill_mid_share_ct', 'rd_phase_kill_late_share_ct', + 'rd_phase_death_early_share_ct', 'rd_phase_death_mid_share_ct', 'rd_phase_death_late_share_ct' + ) }}
@@ -1000,54 +1091,7 @@ document.addEventListener('DOMContentLoaded', function() { const phaseCanvas = document.getElementById('phaseChart'); if (phaseCanvas) { - const ctxPhase = phaseCanvas.getContext('2d'); - new Chart(ctxPhase, { - type: 'bar', - data: { - labels: ['Early', 'Mid', 'Late'], - datasets: [ - { - label: 'Kills', - data: [ - {{ features.get('rd_phase_kill_early_share', 0) }}, - {{ features.get('rd_phase_kill_mid_share', 0) }}, - {{ features.get('rd_phase_kill_late_share', 0) }} - ], - backgroundColor: 'rgba(124, 58, 237, 0.55)' - }, - { - label: 'Deaths', - data: [ - {{ features.get('rd_phase_death_early_share', 0) }}, - {{ features.get('rd_phase_death_mid_share', 0) }}, - {{ features.get('rd_phase_death_late_share', 0) }} - ], - backgroundColor: 'rgba(148, 163, 184, 0.55)' - } - ] - }, - options: { - responsive: true, - maintainAspectRatio: false, - scales: { - y: { - beginAtZero: true, - suggestedMax: 1, - ticks: { - callback: (v) => `${Math.round(v * 100)}%` - } - } - }, - plugins: { - legend: { display: true, position: 'bottom' }, - tooltip: { - callbacks: { - label: (ctx) => `${ctx.dataset.label}: ${(ctx.parsed.y * 100).toFixed(1)}%` - } - } - } - } - }); + phaseCanvas.remove(); } const weaponTop = JSON.parse({{ (features.get('rd_weapon_top_json', '[]') or '[]') | tojson }});