Jamie PackerProjects

Copyright © 2026 Jamie Packer. All rights reserved.

Behavioural Modelling & Classification in Counter-Strike

January 2026

An analytical exploration into professional Counter-Strike 2 player behaviour. Using a custom dataset to quantify abstract behavioural patterns and predict player 'roles' with Machine Learning.

Data AnalysisMachine LearningFeature EngineeringStatisticsPythonCounter-Strike
GitHub Repo
Behavioural Modelling & Classification in Counter-Strike screenshot

TL;DR

Project Goal: To determine if player "roles" in Counter-Strike are measurable behavioural archetypes that can be classified using machine learning, rather than just theoretical concepts.

Potential Utility: This work demonstrates a clear proof-of-concept for automated role labelling, applicable when manual annotation is unfeasible, such as on commercial analytics platforms or for large datasets.

Key Outcomes:

  • Data Processing & Feature Engineering: Processed over 1,000 match replay files (>400GB) to engineer a robust dataset of behavioural features, representing aspects of 'playstyle' such as aggression, trading, and positioning.
  • Quantifying Behaviour: Used statistical analysis (including visualisation, hypothesis testing and regression analysis) to formally model behavioural differences across roles, providing empirical validation for domain intuition (e.g. verifying how specific roles differ in positioning).
  • Role Classification: Developed a supervised learning model as a proof-of-concept for automating role labelling. Achieved ~0.92 (T-Side), ~0.76 (CT-Side) F1-Macro scores, demonstrating that player roles are statistically distinct and enabling the algorithmic identification of mislabelled data.
  • Cluster Analysis: Unsupervised clustering (no role labels used in fitting) recovered groups that align with expert roles, with clearer structure on T-side than CT-side. This validates that these "roles" have real statistical backing in the data, even with just 6 behavioural features.

Introduction

Counter-Strike (CS) has a rich history of competition spanning over two decades; it functions both as a casual game and a high-stakes sport with a healthy professional circuit (exceeding $30 million in annual prize pools). Each match of Counter-Strike acts as a complex system, with 5 players (agents) split onto two sides and constrained by a virtual arena. Out of this system, player "roles" have emerged as optimal ways to play the game. Similar to positions in football, terms like "Lurker" and "Anchor" describe a player's functional responsibility within the team structure.

One motivation for this project is that while these roles are widely discussed, they are determined largely by the "eye test" or community consensus. They do not appear on the scoreboard. Quantifying these behaviours allows us to add rigour to the ongoing discussion in the scene. For example, we can move from debating if a player is an "Entry-Fragger" to measuring exactly where they sit in on a statistical spectrum of aggression and why.

Beyond the theory, automated classification holds potential for significant commercial value. Services like Leetify or Scope.gg rely on providing users with a sense of "Identity". A tool that can mathematically validate a user's playstyle and tell them "You play like an Anchor" or "You played like (Pro Player) that match" could be a powerful engagement driver.


The Data

Source & Scale

The dataset for this project was constructed via a local ETL process (Python) using raw match replay files (.dem) from the 2024 competitive season. To ensure high-quality "labels" for the roles, the data was restricted to Tier 1 Events (Majors and International LANs with >$250k prize pools), filtering out lower-tier matches where roles may not be recorded by my source or may be less structured than in more developed teams.

  • Total Demos Processed: ~1,150 Maps
  • Total Raw Size: >416 GB
  • Parsing Tool: awpy (Python)

Parsing binary files at this scale presented significant engineering challenges, including handling corrupted demos, technical pauses, and the sheer computational cost of processing hundreds of gigabytes of semi-structured event data into usable pandas DataFrames.

The data processing pipeline: from raw .dem files to an aggregated player-feature matrix.

Feature Engineering: Behaviour over Outcome

The core philosophy of this feature set was to quantify style, not skill. Most available statistics (K/D ratio, ADR, Win %) measure performance. I deliberately excluded these to ensure that a "good" Anchor and a "bad" Anchor would still be grouped together based on how they played, rather than their success.

Constraint: I explicitly excluded the potential feature "% of kills with sniper rifles". Whilst this would have considerably improved classification scores, out of research interest I wanted to see if the model could identify an AWPer purely by how they behave, not just by the gun they hold.

I engineered three categories of behavioural features:

CategoryFeatureCaptures
AggressionTAPD (Time Alive Per Death)Do they die early or survive late?
OAP (Opening Attempt %)How often do they take the first duel of the round?
Teamwork (Trading)PODT (Proportion of Deaths Traded)When they die, is a teammate close enough to avenge them?
POKT (Proportion of Kills which were Trades)Are they the ones doing the avenging?
Positioning (Novel)ADNT (Avg Distance to Nearest Teammate)Are they a "pack" player or a lone wolf?
ADAT (Avg Distance to Avg Teammate)Do they play central or peripheral angles?

Normalising Geometry:

One major challenge with positional data is that maps have different sizes and geometries (e.g., Nuke has more verticality, Overpass is large). A distance of 100 units can mean something different on each map.

To solve this, the positional metrics (ADNT/ADAT) are team-relative. For every sampled tick, players were ranked 1–5 based on their distance from teammates, then normalised to (closest) 0.2 <-> 1.0 (farthest).

The Target Labels

To supervise the analysis, I used expert role labels from Harry Richards (HLTV Analyst), whose Positions Databases are a gold standard in the community. The dataset covers these primary roles: Anchor, Rotator, Spacetaker, Lurker, and AWPer.

It is important to note that these labels are "fuzzy". Unlike positions in sports like Baseball (where a Pitcher is explicitly defined by the rules of the game), or non-competitive videogames such as "Overwatch", Counter-Strike roles are strategic concepts. A "Lurker" might group up with the team for a specific execute. This ambiguity makes the classification task significantly harder (and more interesting) than a simple rule-based sort.

Dataset available on: Kaggle / Hugging Face / GitHub


Exploratory Analysis

Interactive ReportNotebook

Before building any models, I needed to answer a fundamental question:

How many matches (maps) does a player need before their playstyle metrics stabilise?

Using bootstrap resampling to estimate measurement uncertainty, I established a 40-map threshold. Below this, behavioural estimates are too noisy to distinguish signal from variance. This filtered the dataset to a stable cohort of 84 professional players, enough to draw meaningful conclusions while ensuring each player's statistics are reliable.

Side Asymmetry: T vs CT

One of the clearest patterns in the data is the difference between attacking (T) and defending (CT) sides:

  • T-side players trade more consistently: Both the proportion of deaths that get traded (+4.6pp) and the proportion of kills that are trades (+7.6pp) are significantly higher on T-side. This reflects the coordinated nature of T-side executes: attackers push together, ensuring deaths are avenged.
  • T-side shows greater behavioural diversity: The standard deviation across most features is wider on T, while CT distributions are tighter. Defenders fall into more constrained patterns; attackers have more room for individual expression.
  • Yet T-side roles are more distinct: Using Mahalanobis distance (a multivariate measure of "how different" each role is from the others), T-side roles showed clearer separation. The most distinct role? T-side AWPers (D² = 4.40), whose weapon constraints enforce a recognisable playstyle across all players.

This asymmetry makes tactical sense: T-side strategies are executed, CT-side strategies are reactive.

The Positioning Problem (More Feature Engineering)

Early analysis revealed a problem: the two positioning metrics, ADNT (distance to nearest teammate) and ADAT (distance from team centre), were highly correlated (r = 0.92 on T-side, 0.78 on CT-side). This makes intuitive sense: players far from their nearest teammate tend to be far from the team centre.

Interactive scatter plot of players by ADNT and ADAT, with roles colour labelled

This correlation pointed to an interesting question:

Do some roles position further from the centre than their isolation alone would predict?

To answer this, I decomposed the relationship using linear regression. By fitting ADAT ~ ADNT for each side and extracting the residuals, I created an orthogonal positioning feature that captures "central vs peripheral tendency" independent of isolation.

The results were revealing:

  • Lurkers (T) and Anchors (CT) consistently position further from the team centre than their isolation predicts, holding off-angles and peripheral positions.
  • AWPers (CT) show the opposite: they stay closer to the centre despite isolation, holding fixed sightlines with team support nearby.
  • Rotators (CT) showed the highest variance in this residual, reflecting their adaptive, context-dependent positioning.

This feature engineering step reduced multicollinearity while preserving (and clarifying) the positional signal that distinguishes roles.


Classifying Roles

Interactive ReportNotebook

If roles are behaviourally distinct, can a machine learning model predict them purely from statistics?

Yeah.

And looking at how can tell us even more about how pros play!

I tested four algorithms (Logistic Regression, SVM, Random Forest, and XGBoost) using nested cross-validation (4 splits × 20 repeats = 80 folds) to ensure robust performance estimates on a small dataset. The results revealed a fundamental difference between the two sides of the game.

T-Side: Linear Separability

Champion Model: Logistic Regression | F1-Macro: ~0.92

Logistic regression coefficients showing how each feature influences role classification on T-side. Positive values push predictions towards that role, negative values push away.

The simplest model won. T-side roles are linearly separable: a straight line (or hyperplane) can cleanly divide AWPers, Lurkers, and Spacetakers in feature space. Complex ensembles like XGBoost offered no improvement.

This means T-side role identities are crisp: positioning metrics (ADNT, ADAT Residual) combined with aggression (OAP, TAPD) fully define the archetypes. The model's coefficients tell a clear story:

  • Lurkers: High isolation (ADNT), high trade-kill participation (POKT)
  • Spacetakers: High opening attempts (OAP), low survival time (TAPD)
  • AWPers: Low isolation (pack play), high survival time

CT-Side: Non-Linear Boundaries

Champion Model: Random Forest | F1-Macro: ~0.75

SHAP beeswarm plots showing how features influence CT-side role predictions. Each dot represents a player; red indicates high feature values, blue indicates low values. Position along the x-axis shows impact on prediction.

CT-side required more complexity. Logistic Regression plateaued at F1 ~0.67, while Random Forest's non-linear decision boundaries pushed performance to ~0.75.

One of the limiting factors was Rotator vs AWPer confusion. These roles share similar positioning profiles on defence, and our features struggle to distinguish them. Interestingly, trade metrics (PODT, POKT) provided almost no discriminatory value on CT-side. Defensive role identity is almost entirely defined by where you play, not how you trade.

The "Ambiguous Role" Test

To test whether the model struggled with features or fuzzy definitions, I ran a sensitivity analysis excluding the hybrid roles (Half-Lurker on T, Mixed on CT).

The result: F1 improved by +0.28 (T) and +0.21 (CT).

This confirms that "Core" roles (Lurker, Spacetaker, Anchor, Rotator, AWPer) are statistically distinct archetypes. The hybrid roles weren't necessarily poorly-captured; they seem to genuinely blur the boundaries between playstyles.

The Sniper-Captain Confound

The most interesting finding came from analysing misclassifications.

Four CT-side Rotators (biguzera, chopper, bLitz, and apEX) were misclassified as AWPers, sometimes confidently. What do they have in common? They're all In-Game Leaders (IGLs).

Digging into IGL feature values revealed the cause: almost all have unusually low ADAT Residuals, meaning they positioned closer to the team centre than typical Rotators. This "central positioning" pattern mimics the spacing profile of CT AWPers.

Comparison of ADAT Residual values for IGLs versus the wider CT role distribution. IGLs across all roles show a clear tendency towards central positioning (lower residual values).

Why would IGLs play centrally? Information flow. Central positions (think: Connector on Mirage, Middle on Ancient) allow the IGL to process information from both bombsites without relying solely on callouts. The trade-off is that these are high-engagement areas typically reserved for star players (often AWPers), which contributes to why IGLs are often criticised for underwhelming stats.

This isn't a novel discovery. Analyst Harry Richards documented the "supportive rotator" IGL trend in 2024. But seeing the model independently surface this pattern validates both the features and the classification approach.


Cluster Analysis

Interactive ReportNotebook

If you throw the labels away, do playstyles still naturally form groups, and do those groups look like the roles analysts already talk about?

Mostly, yes!

Interactive 3D PCA (T-side). Use the buttons to colour by Role, Ward cluster, or GMM cluster.

I used the same Orthogonal feature set as before (behavioural stats plus the isolation and centrality residuals), ran T and CT separately, and dropped the hybrid labels (Half-Lurker on T, Mixed on CT) so the geometry reflects the core archetypes. Two methods: Hierarchical (Ward) for hard partitions, and a Gaussian Mixture Model (GMM) for soft assignments. I fixed k = 3 (one cluster per core role per side) after checking silhouette scores across k = 2 to 6 to check if it was sensible.

Soft groups

Silhouette at k = 3 was about 0.17-0.18 on T and 0.14-0.15 on CT. So there is structure, but the groups overlap. Spacetakers aren't miles from everyone else, and AWPers share some behavioural DNA with other roles. That is roughly how CS works in practice: roles bleed at the edges.

BIC preferred tied covariance for the T-side GMM (three archetypes in different regions of a similarly shaped cloud) and spherical on CT (tighter, less elongated defensive structure).

Agreement with expert roles

Ward dendrogram of T-side players. Leaf colours are expert roles; cutting at k=3 the clusters largely maps onto each role Spacetaker, Lurker, and AWPer.

Using Adjusted Rand Index (chance-corrected; random partitions score near 0):

  • T-side: 0.68 (Ward) / 0.61 (GMM)
  • CT-side: 0.31 (Ward) / 0.30 (GMM)

T-side is roughly twice as strong. Ward largely pulls out Spacetaker, Lurker, and AWPer blocks (the dendrogram above is the T-side view of that). On CT, Anchors separate more cleanly, while Rotators and AWPers mix. Same porosity the classifier hit, this time without ever seeing the labels during fitting.