"""
Gokul Ramanan
panel_creation_predictions.py
8/6/2025
Description: Creates a panel dashboard to predict the number of runs some
of the top young cricketers will score in 5 years for a specific format. Minimum
Criteria for the prediction is 5 years of international experience in the format.
"""
import panel as pn
from runs_api import RUNSAPI
import plotly.graph_objects as go
import plotly.colors as pc
from statsmodels.nonparametric.smoothers_lowess import lowess
import pandas as pd
df_one = pd.read_csv("prediction_players.csv")
df_two = pd.read_csv("prediction.csv")
df_pred_insert = pd.DataFrame({
"Year": 2030,
"Country": df_two["Country"],
"Name": df_two["Name"],
"Format": df_two["Format"],
"Mat": round(df_two["Pred_Innings"] / df_two["Inns/Match"], 0),
"Inns": df_two["Pred_Innings"].round(0).astype(int),
"NO": 0, # Assuming none are not-outs for simplicity
"Runs": df_two["Predicted_Runs"].round(1),
"HS": None,
"Avg": df_two["Final_Runs_Per_Inning"].round(2),
"BF": None,
"SR": None,
"100s": 0,
"50s": 0,
"0s": 0,
"4s": 0,
"6s": 0
})
# Concatenate the new rows to the original df_one
df_combined = pd.concat([df_one, df_pred_insert], ignore_index=True)
df_combined["Is_Prediction"] = df_combined["Year"] == 2030
df_combined.to_csv("add_preds.csv", index = False)
# Loads javascript dependencies and configures Panel (required)
pn.extension()
# WIDGET DECLARATIONS
api = RUNSAPI()
api.load_runs("add_preds.csv")
# Search Widgets
format_select = pn.widgets.CheckBoxGroup(name="Format", options=["test", "odi", "t20i"], value = ["test", "odi", "t20i"])
country_select = pn.widgets.MultiSelect(name="Country", options=sorted(api.runs["Country"].unique().tolist()), size=6)
year_slider = pn.widgets.IntRangeSlider(name="Year Range", start=api.runs["Year"].min(), end=api.runs["Year"].max(), step=1)
top_n_slider = pn.widgets.IntSlider(name="Top N Players", start=1, end=17, value=17)
player_select = pn.widgets.MultiChoice(
name="Select Players",
options=sorted(api.runs["Name"].unique().tolist()),
placeholder="Choose players to compare (optional)..."
)
career_length_slider = pn.widgets.IntRangeSlider(
name="Career Length (Years)",
start=1,
end=25,
step=1,
value=(1, 25)
)
all_formats_toggle = pn.widgets.Checkbox(
name="Only Include Players in All 3 Formats",
value=False
)
not_formats_toggle = pn.widgets.Checkbox(
name="Don't Include Players in All 3 Formats",
value=False
)
# Plotting widgets
width = pn.widgets.IntSlider(name = 'Width', start = 250, end = 2000, step = 250, value = 1500)
height = pn.widgets.IntSlider(name = 'Height', start = 200, end = 2500, step = 100, value = 800)
color_by = pn.widgets.Select(
name="Color By",
options=["Name", "Country", "Debut Bin"],
value="Name"
)
career_align_toggle = pn.widgets.Checkbox(
name="Align Careers to Year 0",
value=False
)
detailed_tooltip = pn.widgets.Checkbox(
name="Show Detailed Hover Info",
value=True
)
plot_metric_select = pn.widgets.RadioButtonGroup(
name="Plot Metric",
options=["Cumulative Runs", "Cumulative Batting Average"],
button_type="primary",
value="Cumulative Runs"
)
theme_map = {
"white": "plotly_white",
"dark": "plotly_dark",
"gray1": "ggplot2",
"gray2": "seaborn",
"white2": "simple_white"
}
theme_select = pn.widgets.RadioButtonGroup(
name="Plot Theme",
options=list(theme_map.keys()),
button_type="success",
value="dark"
)
# CALLBACK FUNCTIONS
def get_plot(format_select, country_select, year_slider, top_n_slider, player_select, career_length_slider, width,
height, theme_select_value, plot_metric_select_value, color_by_value, detailed_tooltip_value,
align_career_value, all_formats_toggle, not_formats_toggle):
"""
Generate a cumulative runs line chart based on filter selections.
Parameters:
format_select (list): Selected match formats (e.g., ['odi', 'test']).
country_select (list): List of selected countries.
year_slider (list): List of [start_year, end_year] to filter data by year.
top_n_slider (int): Number of top players to include based on total runs.
width (int): Plot width in pixels.
height (int): Plot height in pixels.
theme_select_value (str): Theme name corresponding to Plotly templates.
Returns:
panel.pane.Plotly or panel.pane.Markdown: Plotly pane if data exists,
otherwise a message pane.
"""
if plot_metric_select_value == "Cumulative Runs":
y_col = "cumulative_format_runs"
hover = "Runs"
ranking_metric = "Runs"
elif plot_metric_select_value == "Cumulative Batting Average":
y_col = "cumulative_format_average"
hover = "Average"
ranking_metric = "Average"
df = api.apply_filters(formats=format_select, countries=country_select, year_range=year_slider,
top_n_players=top_n_slider, player_select_value = player_select, ranking_metric=ranking_metric,
career_length_slider = career_length_slider, only_all_formats = all_formats_toggle,
not_all_formats=not_formats_toggle)
if plot_metric_select_value == "Cumulative Strike Rate":
df = df[~df["Name"].isin(["Sachin Tendulkar", "Allan Border", "Javed Miandad", "Desmond Haynes",
"Aravinda de Silva", "Mohammed Azharuddin", "Viv Richards", "Marvin Atapattu",
"Sunil Gavaskar", "Saleem Malik", "Gordon Greenidge", "Arjuna Ranatunga",
"Richie Richardson", ])]
df = df[df["cumulative_SR"].notnull()]
if df.empty:
return pn.pane.Markdown("### No data for selected filters.", width=700)
fig = go.Figure()
grouped = df.groupby("Name")
# Choose a color palette (20 vibrant colors)
color_palette = (
pc.qualitative.Set3 + pc.qualitative.Set2 +
pc.qualitative.Bold + pc.qualitative.Pastel +
pc.qualitative.Dark2 + pc.qualitative.Safe
)
# Get unique labels from your dataframe
unique_labels = df[color_by_value].unique()
color_map = {label: color_palette[i % len(color_palette)] for i, label in enumerate(sorted(unique_labels))}
x_col = "Career Year" if align_career_value else "Year"
x_axis_title = "Career Year" if align_career_value else "Year"
for name, group in grouped:
color_label = group[color_by_value].iloc[0]
color = color_map.get(color_label, "#000000")
custom_data = group[["cumulative_innings", "cumulative_matches", "cumulative_100s",
"cumulative_50s"]].values
if detailed_tooltip_value:
hovertemplate = (
f"{name}
"
"Year: %{x}
"
f"{hover}: %{{y}}
"
"Innings: %{customdata[0]}
"
"Matches: %{customdata[1]}
"
f"{color_by.name}: {color_label}"
)
else:
hovertemplate = (
f"{name}
"
f"Year: %{{x}}
"
f"{hover}: %{{y}}
"
f"{color_by.name}: {color_label}"
)
historical = group[group["Is_Prediction"] == False]
predicted = group[group["Is_Prediction"] == True]
# Historical (solid)
fig.add_trace(go.Scatter(
x=historical[x_col],
y=historical[y_col],
mode='lines+markers',
name=name,
customdata=historical[["cumulative_innings", "cumulative_matches", "cumulative_100s", "cumulative_50s"]],
line=dict(color=color),
legendgroup=color_label,
hovertemplate=hovertemplate
))
# Prediction (dot)
if not predicted.empty:
fig.add_trace(go.Scatter(
x=predicted[x_col],
y=predicted[y_col],
mode='markers',
name=f"{name} (Predicted)",
customdata=predicted[["cumulative_innings", "cumulative_matches", "cumulative_100s", "cumulative_50s"]],
line=dict(color=color, dash='dot'),
marker=dict(size=10, symbol='diamond'),
legendgroup=color_label,
hovertemplate=hovertemplate.replace("", "[Predicted] ")
))
if align_career_value:
curve_df = df[[x_col, y_col]].dropna()
if not curve_df.empty:
smoothed = lowess(endog=curve_df[y_col], exog=curve_df[x_col], frac=0.2)
fig.add_trace(go.Scatter(
x=smoothed[:, 0],
y=smoothed[:, 1],
mode='lines',
name="Best Fit Curve",
line=dict(width=4, color='black', dash='dot'),
hoverinfo='skip',
showlegend=True
))
fig.update_layout(
title=f"Cumulative International {hover} Over Time",
xaxis_title=x_axis_title,
yaxis_title=f"Cumulative {hover}",
width=width,
height=height,
showlegend=True,
template= theme_map[theme_select_value]
)
return pn.pane.Plotly(fig)
def get_catalog(format_select, country_select, year_slider, top_n_slider,player_select,career_length_slider, all_formats_toggle,
not_formats_toggle):
"""
Generate an interactive data table of filtered run statistics.
Parameters:
format_select (list): Selected match formats (e.g., ['t20i']).
country_select (list): List of selected countries.
year_slider (tuple): (start_year, end_year) year range filter.
top_n_slider (int): Number of top run-scorers to include.
Returns:
panel.widgets.Tabulator: A paginated and scrollable data table
of the filtered DataFrame.
"""
df = api.apply_filters(formats=format_select, countries=country_select, year_range=year_slider,
top_n_players=top_n_slider, player_select_value= player_select,
career_length_slider = career_length_slider, only_all_formats = all_formats_toggle,
not_all_formats= not_formats_toggle)
table = pn.widgets.Tabulator(df, selectable=False, pagination = 'local', page_size = 20)
return table
# CALLBACK BINDINGS (Connecting widgets to callback functions)
plot = pn.bind(get_plot, format_select, country_select, year_slider, top_n_slider, player_select,
career_length_slider, width, height, theme_select, plot_metric_select, color_by, detailed_tooltip,
career_align_toggle, all_formats_toggle, not_formats_toggle)
catalog = pn.bind(get_catalog, format_select, country_select, year_slider, top_n_slider, player_select,
career_length_slider, all_formats_toggle, not_formats_toggle)
# DASHBOARD WIDGET CONTAINERS ("CARDS")
card_width = 320
search_card = pn.Card(
pn.Column(
# Widget 1
format_select,
# Widget 2
country_select,
# Widget 3
year_slider,
# Widget 4
top_n_slider,
# Widget 5
player_select,
# Widget 6
career_length_slider,
all_formats_toggle,
not_formats_toggle
),
title="Search", width=card_width, collapsed=False
)
plot_card = pn.Card(
pn.Column(
career_align_toggle,
# Default Widget
plot_metric_select,
# Default Widget 2
color_by,
# Default Widget 3
detailed_tooltip,
# Widget 1
width,
# Widget 2
height,
# Widget 3
theme_select
),
title="Plot", width=card_width, collapsed=False
)
# LAYOUT
layout = pn.template.FastListTemplate(
title="Top Prospective International Cricket Batters: Predicted Runs by 2030",
sidebar=[
search_card,
plot_card,
],
theme_toggle=False,
main=[
pn.Tabs(
("Table", catalog), # Replace None with callback binding
("Time Series", plot), # Replace None with callback binding
active=1 # Which tab is active by default?
)
],
header_background='#a93226'
).servable()
layout.show()