Batting Intent Analysis in IPL 2025 with Python

In modern T20 cricket, especially in high-stakes tournaments like the IPL, understanding a batter’s intent is as important as analyzing their strike rate or total runs. Batting intent reflects how aggressively a player approaches different phases of the game. In this article, I’ll perform a detailed Batting Intent Analysis with Python using ball-by-ball data from a recent IPL match between RCB and DC in 2025.

Batting Intent Analysis in IPL 2025: Overview

We’ll be using ball-by-ball data from a recent IPL match between RCB and DC, which includes details like over number, batter, bowler, runs scored, and wicket events. This rich, granular dataset allows us to analyze batting patterns across different phases of the game.

The scope of our analysis is to understand batting intent: how players approach the game in various situations; by examining strike rates, boundary percentages, dot ball rates, and over-wise performance, ultimately uncovering insights that can influence real-time strategy and decision-making.

You can download the dataset from here.

Batting Intent Analysis in IPL 2025 with Python: Getting Started

Now, let’s get started with the task of Batting Intent Analysis in IPL 2025 by importing the necessary Python libraries and the dataset:

import pandas as pd
import seaborn as sns
from math import pi

df = pd.read_csv('/content/ipl_match_1473461_deliveries.csv')
df_copy = df.copy()
df
IPL 2025 match data

Before moving forward, let’s categorize each delivery into match phases: Powerplay, Middle Overs, and Death Overs; based on the over number:

# add game phase based on overs
def get_phase(over):
    if over < 6:
        return 'Powerplay'
    elif 6 <= over < 15:
        return 'Middle Overs'
    else:
        return 'Death Overs'

df_copy['phase'] = df_copy['over'].apply(get_phase)

Here, we defined a function get_phase() that assigns a phase label based on the over number: overs 0 to 5 as Powerplay, 6 to 14 as Middle Overs, and 15 onwards as Death Overs. Then, we applied this function to the over column to create a new phase column in the dataset.

Now, let’s calculate each batter’s strike rate across different phases of the game and visualize their batting intent:

import plotly.express as px
import pandas as pd

batting_intent = (
    df_copy.groupby(['batter', 'phase'])
    .agg(balls_faced=('runs_batter', 'count'),
         total_runs=('runs_batter', 'sum'))
    .reset_index()
)
batting_intent['strike_rate'] = (batting_intent['total_runs'] / batting_intent['balls_faced']) * 100

batting_intent = batting_intent[batting_intent['balls_faced'] >= 5]

batting_intent.sort_values(by=['batter', 'phase'], inplace=True)

fig = px.bar(
    batting_intent,
    x='batter',
    y='strike_rate',
    color='phase',
    barmode='group',
    title='📊 Batting Intent: Strike Rate Across Phases of the Game',
    labels={'strike_rate': 'Strike Rate', 'batter': 'Batter'},
    hover_data={
        'balls_faced': True,
        'total_runs': True,
        'strike_rate': ':.2f',
        'phase': True
    }
)

fig.update_layout(
    xaxis_tickangle=-45,
    title_font_size=20,
    legend_title='Game Phase',
    plot_bgcolor='rgba(0,0,0,0)',
    paper_bgcolor='white',
    bargap=0.2,
    margin=dict(l=40, r=40, t=80, b=100)
)

fig.update_traces(marker_line_width=1, marker_line_color='black')

fig.show()
Batting Intent: Strike Rate Across Phases of the Game

T Stubbs and KL Rahul showcased aggressive intent in the Death Overs, with strike rates exceeding 220 and 220, respectively, indicating high-impact finishing roles. PD Salt and AR Patel were strong starters with strike rates over 140 in the Powerplay.

Team-Wise Batting Intent Across Match Phases

Now, let’s compare the batting intent of both teams across different match phases using strike rate as a metric:

df_copy['batting_team'] = df_copy['team']

team_phase_intent = (
    df_copy.groupby(['batting_team', 'phase'])
    .agg(balls_faced=('runs_batter', 'count'),
         total_runs=('runs_batter', 'sum'))
    .reset_index()
)

team_phase_intent['strike_rate'] = (team_phase_intent['total_runs'] / team_phase_intent['balls_faced']) * 100

fig = px.bar(
    team_phase_intent,
    x='phase',
    y='strike_rate',
    color='batting_team',
    barmode='group',
    title='🏏 Team-wise Batting Intent Across Match Phases',
    labels={
        'phase': 'Match Phase',
        'strike_rate': 'Strike Rate',
        'batting_team': 'Team'
    },
    hover_data={
        'balls_faced': True,
        'total_runs': True,
        'strike_rate': ':.2f'
    }
)

fig.update_layout(
    xaxis_title='Match Phase',
    yaxis_title='Strike Rate',
    title_font_size=20,
    legend_title_text='Batting Team',
    bargap=0.25,
    plot_bgcolor='rgba(0,0,0,0)',
    paper_bgcolor='white',
    margin=dict(l=40, r=40, t=80, b=60)
)

fig.update_traces(marker_line_width=1, marker_line_color='black')

fig.show()
Batting Intent Analysis: Team-wise Batting Intent Across Match Phases

The graph reveals a stark contrast in batting intent between Delhi Capitals and Royal Challengers Bengaluru across different match phases. Delhi Capitals showed a clear surge in aggression during the Death Overs, registering a strike rate close to 240, significantly outpacing RCB.

In contrast, RCB were more aggressive in the Powerplay with a strike rate of around 135, while Delhi started more cautiously. Across Middle Overs, Delhi again maintained a higher tempo than RCB, suggesting better momentum-building and acceleration strategies. Overall, DC dominated the later stages, while RCB leaned on early aggression.

Boundary % vs Dot Ball % per Batter

Now, let’s analyze each batter’s ability to rotate strike and score boundaries by calculating their Boundary % and Dot Ball %:

import plotly.graph_objects as go

df_copy['ball_outcome'] = df_copy['runs_batter'].apply(
    lambda x: 'Dot' if x == 0 else ('Boundary' if x >= 4 else 'Run')
)

batter_outcome_stats = (
    df_copy.groupby('batter')
    .ball_outcome.value_counts(normalize=True)
    .unstack()
    .fillna(0) * 100
).reset_index()

balls_faced = df_copy.groupby('batter').size().reset_index(name='balls_faced')
batter_outcome_stats = batter_outcome_stats.merge(balls_faced, on='batter')
batter_outcome_stats = batter_outcome_stats[batter_outcome_stats['balls_faced'] >= 10]

batter_outcome_stats = batter_outcome_stats.sort_values(by='Boundary', ascending=False)

fig = go.Figure()

fig.add_trace(go.Bar(
    x=batter_outcome_stats['batter'],
    y=batter_outcome_stats['Boundary'],
    name='Boundary %',
    marker_color='green',
    hovertemplate='%{x}<br>Boundary %: %{y:.2f}<extra></extra>'
))

fig.add_trace(go.Bar(
    x=batter_outcome_stats['batter'],
    y=batter_outcome_stats['Dot'],
    name='Dot Ball %',
    marker_color='red',
    hovertemplate='%{x}<br>Dot Ball %: %{y:.2f}<extra></extra>'
))

fig.update_layout(
    title='🏏 Boundary % vs Dot Ball % per Batter',
    xaxis_title='Batter',
    yaxis_title='Percentage (%)',
    barmode='group',
    bargap=0.25,
    plot_bgcolor='rgba(0,0,0,0)',
    paper_bgcolor='white',
    legend_title_text='Ball Outcome',
    xaxis_tickangle=-45,
    margin=dict(l=40, r=40, t=80, b=100)
)

fig.show()
Batting Intent Analysis: Boundary % vs Dot Ball % per Batter

PD Salt stands out with the most balanced approach: high boundary rate (~39%) and relatively low dot ball %; indicating consistent attacking intent. TH David, despite a strong boundary presence, also has a high dot ball percentage, reflecting a high-risk, high-reward style.

On the contrary, JM Sharma’s intent appears defensive or ineffective, with a dot ball percentage of over 70% and negligible boundaries. Batters like KL Rahul and V Kohli show moderate boundary rates but also face a significant number of dot balls, suggesting a more conservative or accumulative style of play.

Over-Wise Run Progression of Top 4 Batters

Now, let’s track how the top 4 run-scorers progressed throughout the innings by analyzing their over-wise scoring patterns:

top_batters = (
    df_copy.groupby('batter')['runs_batter']
    .sum()
    .sort_values(ascending=False)
    .head(4)
    .index.tolist()
)

batters_progression = df_copy[df_copy['batter'].isin(top_batters)]
batters_overwise = (
    batters_progression.groupby(['batter', 'over'])
    .agg(runs_in_over=('runs_batter', 'sum'))
    .reset_index()
)

fig = px.line(
    batters_overwise,
    x='over',
    y='runs_in_over',
    color='batter',
    markers=True,
    title='📈 Over-wise Run Progression of Top 4 Batters',
    labels={
        'over': 'Over',
        'runs_in_over': 'Runs in Over',
        'batter': 'Batter'
    },
    hover_data={
        'over': True,
        'runs_in_over': True,
        'batter': True
    }
)

fig.update_layout(
    xaxis=dict(tickmode='linear'),
    yaxis_title='Runs Scored in Over',
    legend_title='Top Batters',
    title_font_size=20,
    plot_bgcolor='rgba(0,0,0,0)',
    paper_bgcolor='white',
    margin=dict(l=40, r=40, t=80, b=60),
    hovermode='x unified'
)

fig.show()
Over-wise Run Progression of Top 4 Batters

PD Salt demonstrated explosive intent upfront, scoring heavily in the first three overs, especially with a 24-run blitz in the 2nd over. KL Rahul adopted a steadier approach, gradually accelerating through the middle overs and peaking with a 22-run over around the 14th. T Stubbs showed late-inning aggression, particularly in overs 14 to 16, aligning with a finisher’s role. TH David made his impact during the death overs (17–19), consistently scoring above 15 runs per over, indicating high-impact finishing intent.

Overall, each batter’s run pattern reflects their strategic roles in the innings.

Correlating Batting Tempo with Wicket Falls

Now, let’s compare how runs and wickets fluctuated over each over to understand momentum shifts during the innings:

wickets_df = df_copy[df_copy['player_out'].notna()]
wickets_by_over = wickets_df.groupby('over').size().reset_index(name='wickets')

runs_by_over = df_copy.groupby('over')['runs_batter'].sum().reset_index(name='total_runs')

overwise_analysis = pd.merge(runs_by_over, wickets_by_over, on='over', how='left').fillna(0)

fig = go.Figure()

fig.add_trace(go.Bar(
    x=overwise_analysis['over'],
    y=overwise_analysis['total_runs'],
    name='Runs Scored',
    marker_color='skyblue',
    hovertemplate='Over %{x}<br>Runs: %{y}<extra></extra>'
))

fig.add_trace(go.Scatter(
    x=overwise_analysis['over'],
    y=overwise_analysis['wickets'],
    name='Wickets',
    mode='lines+markers',
    marker=dict(color='red', size=8),
    line=dict(width=2, color='red'),
    yaxis='y2',
    hovertemplate='Over %{x}<br>Wickets: %{y}<extra></extra>'
))

fig.update_layout(
    title='📉 Over-wise Analysis: Runs vs Wickets',
    xaxis=dict(title='Over', tickmode='linear'),
    yaxis=dict(title='Runs Scored'),
    yaxis2=dict(title='Wickets', overlaying='y', side='right'),
    legend_title='Metrics',
    plot_bgcolor='rgba(0,0,0,0)',
    paper_bgcolor='white',
    margin=dict(l=40, r=40, t=80, b=60),
    hovermode='x unified'
)

fig.show()
Over-wise Analysis: Runs vs Wickets

The early overs (2nd and 3rd) indicate explosive intent, with 35+ runs scored, suggesting aggressive Powerplay batting. However, this high-risk approach also led to early wickets, as seen in the 1st and 4th overs. The middle overs (7–10) show fluctuating intent with moderate scoring and occasional wicket losses, indicating a cautious rebuild phase.

The batting side regained momentum in the death overs (13–19) with consistent scoring around 16–27 runs per over, despite losing wickets in the 14th and 17th, reflecting strong finishing intent while balancing aggression with risk.

Batter Profiles Based on Match Performance

Now, let’s build comprehensive performance profiles for each batter, including their strike rate, average, dot ball %, and boundary %:

batter_stats = (
    df_copy.groupby('batter')
    .agg(
        balls_faced=('runs_batter', 'count'),
        total_runs=('runs_batter', 'sum'),
        dismissals=('player_out', lambda x: x.notna().sum())
    )
    .reset_index()
)

outcome_counts = df_copy.groupby(['batter', 'ball_outcome']).size().unstack().fillna(0)
outcome_counts['dot_percent'] = (outcome_counts['Dot'] / outcome_counts.sum(axis=1)) * 100
outcome_counts['boundary_percent'] = (outcome_counts['Boundary'] / outcome_counts.sum(axis=1)) * 100
outcome_counts = outcome_counts[['dot_percent', 'boundary_percent']].reset_index()

batter_profiles = pd.merge(batter_stats, outcome_counts, on='batter')
batter_profiles['strike_rate'] = (batter_profiles['total_runs'] / batter_profiles['balls_faced']) * 100
batter_profiles['average'] = batter_profiles.apply(
    lambda row: row['total_runs'] / row['dismissals'] if row['dismissals'] > 0 else float('inf'), axis=1
)
batter_profiles = batter_profiles[batter_profiles['balls_faced'] >= 10]

Here, we started by calculating basic batting stats: total runs, balls faced, and number of dismissals; for each batter. Then, we break down their outcomes into dot balls and boundaries, computing their percentages relative to total deliveries faced. We merged this with the main stats and calculated the batter’s strike rate and batting average.

To maintain statistical significance, we only considered batters who faced at least 10 balls. The resulting batter_profiles dataset gives a well-rounded view of each player’s performance and intent, helping us distinguish between aggressive finishers, anchors, and low-impact players.

So, let’s visualize the performance profiles of the top 4 batters using radar charts to compare strike rate, dot ball %, and boundary %:

from plotly.subplots import make_subplots
from math import pi, ceil

metrics = ['strike_rate', 'dot_percent', 'boundary_percent']
top_batters_radar = batter_profiles.sort_values(by='strike_rate', ascending=False).head(4).copy()
normalized_profiles = top_batters_radar[['batter'] + metrics].copy()

for metric in metrics:
    max_val = batter_profiles[metric].max()
    normalized_profiles[metric] = (normalized_profiles[metric] / max_val) * 100

normalized_profiles.reset_index(drop=True, inplace=True)

num_batters = len(normalized_profiles)
rows = ceil(num_batters / 2)
cols = 2 if num_batters > 1 else 1

fig = make_subplots(
    rows=rows, cols=cols,
    specs=[[{'type': 'polar'}] * cols for _ in range(rows)],
    subplot_titles=normalized_profiles['batter'].tolist()
)

for i, row in normalized_profiles.iterrows():
    r = row[metrics].tolist() + [row[metrics[0]]]
    theta = metrics + [metrics[0]]

    subplot_row = (i // cols) + 1
    subplot_col = (i % cols) + 1

    fig.add_trace(
        go.Scatterpolar(
            r=r,
            theta=theta,
            fill='toself',
            name=row['batter'],
            hovertemplate='<b>%{theta}</b>: %{r:.1f}<extra></extra>'
        ),
        row=subplot_row, col=subplot_col
    )

fig.update_layout(
    title='🔘 Batter Profiles: Radar Chart of Batting Metrics',
    showlegend=False,
    height=400 * rows,
    polar=dict(
        radialaxis=dict(visible=True, range=[0, 100], showticklabels=False)
    ),
    margin=dict(l=40, r=40, t=100, b=50)
)

fig.show()
Batter Profiles: Radar Chart of Batting Metrics

The radar charts offer a quick comparative view of batting intent across PD Salt, KL Rahul, T Stubbs, and TH David. PD Salt showcases the most balanced attacking profile, combining a high strike rate, strong boundary %, and moderate dot ball %.

KL Rahul, though maintaining a decent strike rate, relies more on boundaries with relatively higher dot ball involvement, indicating a mixed approach. T Stubbs reflects an explosive style with maximum boundary % and minimal dot balls, ideal for finishing roles. On the other hand, TH David shows a high strike rate but also a high dot ball %, indicating sporadic aggression with a risk of inconsistency.

Summary

So, by examining strike rates, boundary and dot ball percentages, over-wise run patterns, and wicket impacts, we decoded how each player approached different match phases. From aggressive Powerplay openers like PD Salt to high-impact finishers like T Stubbs and TH David, and steady anchors like KL Rahul, the data revealed distinct batting styles.

I hope you liked this article on batting intent analysis in IPL 2025 using Python. Feel free to ask valuable questions in the comments section below. You can follow me on Instagram for many more resources.

Aman Kharwal
Aman Kharwal

AI/ML Engineer | Published Author. My aim is to decode data science for the real world in the most simple words.

Articles: 2074

5 Comments

  1. Can you also provided ball by ball dataset of any recent IPL 2025 matches . I explored different websites but didn’t found dataset . It would be really helpful if you provide more ball by ball dataset of recent IPL match.

  2. It would be really helpful if you provide dataset of DC vs GT IPL 2025 match . Thank you for responding.

    • Just an add-on , I want the dataset of RR vs PBKS IPL 2025 match (number 59) and DC vs GT IPL 2025 match (number 60). It would be really helpful .

Leave a Reply

Discover more from AmanXai by Aman Kharwal

Subscribe now to keep reading and get access to the full archive.

Continue reading