Shot Plot and Xg-table in 1 script (Python)

Author:

Jeg har bygget videre på scripts fra de seneste versioner, og har nu samlet funktionerne “Shot Plot” og “Xg-Table” i 1 script, der leverer 2 seperate PNGs, som jeg begge finder visuelt indbydende.
Mangler pt.:
1) At klublogoerne stadig hentes lokalt i stedet via cloud/api
2) At det ikke er lykkedes at få indbygget rundenummeret på hver PNG.
3) At jeg endnu ikke ved hvad jeg skal bruge det 🙂

Eksempel på produkt!

Jeg er tilfreds med de visuelle udtryk og den visuelle opsætning.
En anden mangel er at scriptet åbner alle plots på en gang, hvis man f.eks. beder det om at forholde sig til 32 events….

import os
import requests
import json
import pandas as pd
import matplotlib.pyplot as plt
from PIL import Image
from mplsoccer import VerticalPitch, FontManager
from matplotlib.lines import Line2D

# List of teams and their IDs with short names
teams = [
    {"teamId": 8071, "teamName": "AGF"},
    {"teamId": 8470, "teamName": "AaB"},
    {"teamId": 8595, "teamName": "BIF"},  # Short name for Brøndby IF
    {"teamId": 8391, "teamName": "FCK"},  # Short name for F.C. København
    {"teamId": 8113, "teamName": "FCM"},  # Short name for FC Midtjylland
    {"teamId": 10202, "teamName": "FCN"},  # Short name for FC Nordsjælland
    {"teamId": 9907, "teamName": "LBK"},  # Short name for Lyngby Boldklub
    {"teamId": 8410, "teamName": "RFC"},  # Short name for Randers FC
    {"teamId": 8415, "teamName": "SIF"},  # Short name for Silkeborg IF
    {"teamId": 8487, "teamName": "SJE"},  # Short name for Sønderjyske Fodbold
    {"teamId": 8231, "teamName": "VB"},  # Short name for Vejle Boldklub
    {"teamId": 9939, "teamName": "VFF"},  # Short name for Viborg FF
    {"teamId": 9814, "teamName": "ACH"},  # Short name for AC Horsens
    {"teamId": 10240, "teamName": "Hvidovre"},  # Short name for Hvidovre IF
    {"teamId": 8414, "teamName": "OB"},
]

# Function to display the list of teams
def display_teams(teams):
    print("List of teams and their IDs with short names:")
    for team in teams:
        print(f"Team ID: {team['teamId']}, Team Name: {team['teamName']}")

display_teams(teams)

# Function to get the short name of the team based on teamId
def get_team_short_name(team_id):
    for team in teams:
        if team["teamId"] == team_id:
            return team["teamName"]
    return "Unknown"

# Function to parse rounds input and handle ranges
def parse_rounds(rounds_input):
    rounds = []
    for part in rounds_input.split(','):
        if '-' in part:
            start, end = part.split('-')
            rounds.extend(range(int(start), int(end) + 1))
        else:
            rounds.append(int(part))
    return [str(round) for round in rounds]  # Convert rounds to strings for comparison

# Prompt user to input the rounds they want to fetch event IDs for
rounds_input = input("Enter the rounds you want to fetch event IDs for (comma separated, e.g., 1,2,5 or 1-5): ")
selected_rounds = parse_rounds(rounds_input)

# Prompt user to input the team ID if they want to filter by a specific team (leave blank if not)
team_id = input("Enter the team ID to filter by (leave blank if not applicable): ").strip()

# Step 1: Fetch events
events_url = 'https://api.superliga.dk/events-v2?appName=dk.releaze.livecenter.spdk&access_token=5b6ab6f5eb84c60031bbbd24&env=production&locale=da&seasonId=20962'
response = requests.get(events_url)
events_data = response.json()

# Step 2: Filter finished events from selected rounds and optional team ID
finished_events = [
    event for event in events_data['events']
    if event['statusType'] == 'finished' and event['round'] in selected_rounds
    and (not team_id or event['homeId'] == int(team_id) or event['awayId'] == int(team_id))
]

# Print the list of available events with short team names
print("Available Events:")
for event in finished_events:
    home_short_name = get_team_short_name(event['homeId'])
    away_short_name = get_team_short_name(event['awayId'])
    print(f"{event['eventId']} {home_short_name} vs {away_short_name}")

# Prompt user to select whether to generate for all events or a single event
choice = input("Do you want to generate xG tables for all events or a single event? (all/single): ").strip().lower()

def generate_xg_table(event_id, show_table=True, plot_shots=False, single_event=False):
    try:
        details_url = f'https://api.superliga.dk/opta-stats/event/{event_id}/detail-expected-goals?appName=superligadk&access_token=5b6ab6f5eb84c60031bbbd24&env=production&locale=da'
        response = requests.get(details_url)
        data = response.json()

        home_team_id = data.get('homeId')
        away_team_id = data.get('awayId')
        home_team_name = data.get('homeName')
        away_team_name = data.get('awayName')
        home_score = data['score']['home']
        away_score = data['score']['away']

        # Paths to team logos
        logo_directory = '/home/xxxxxxx/python_scripts/TeamPngs'  
        home_team_logo_path = os.path.join(logo_directory, f'{home_team_id}.png')
        away_team_logo_path = os.path.join(logo_directory, f'{away_team_id}.png')

        # Load team logos
        home_team_logo = Image.open(home_team_logo_path)
        away_team_logo = Image.open(away_team_logo_path)

        # Extract home and away expected goals data
        home_data = pd.DataFrame(data['expectedGoalsData']['home'])
        away_data = pd.DataFrame(data['expectedGoalsData']['away'])

        # Adjusting the coordinates assuming the pitch is 120x80
        home_data['x_adjusted'] = home_data['x'] * 120 / 100
        home_data['y_adjusted'] = home_data['y'] * 80 / 100

        away_data['x_adjusted'] = away_data['x'] * 120 / 100
        away_data['y_adjusted'] = away_data['y'] * 80 / 100

        # Format shot data for table
        home_shots = home_data[['min', 'sec', 'firstName', 'lastName', 'expectedGoalsValue', 'type', 'x_adjusted', 'y_adjusted']].copy()
        home_shots['team'] = home_team_name

        away_shots = away_data[['min', 'sec', 'firstName', 'lastName', 'expectedGoalsValue', 'type', 'x_adjusted', 'y_adjusted']].copy()
        away_shots['team'] = away_team_name

        # Combine home and away shots
        shots = pd.concat([home_shots, away_shots])

        # Sort by minute and second
        shots = shots.sort_values(by=['min', 'sec'])

        # Format xG values to 4 decimal places
        shots['expectedGoalsValue'] = shots['expectedGoalsValue'].map(lambda x: f"{x:.4f}")

        # Create the table plot
        fig, ax = plt.subplots(figsize=(12, 8))
        ax.axis('tight')
        ax.axis('off')

        table_data = shots[['team', 'min', 'sec', 'firstName', 'lastName', 'expectedGoalsValue', 'type']].values
        column_labels = ['Team', 'Min', 'Sec', 'First Name', 'Last Name', 'xG Value', 'Type']

        table = ax.table(cellText=table_data, colLabels=column_labels, cellLoc='center', loc='center')

        # Set table styles
        table.auto_set_font_size(False)
        table.set_fontsize(11)
        table.scale(1.3, 1.3)

        # Adjust column widths
        table.auto_set_column_width([0, 1, 2, 3, 4, 5, 6])

        # Add title with team logos
        fig.suptitle(f" xG Table for {home_team_name} vs {away_team_name} ", fontsize=16, fontweight='bold', y=0.98)

        # Add final score
        fig.text(0.5, 0.92, f"Final Score:  {home_score} - {away_score}", ha='center', fontsize=18, fontweight='bold')

        # Reduce space between title and table
        plt.subplots_adjust(top=0.85)

        # Add team logos to the title
        ax_logo_home = fig.add_axes([0.15, 0.88, 0.1, 0.1], anchor='NE', zorder=1)
        ax_logo_home.imshow(home_team_logo)
        ax_logo_home.axis('off')

        ax_logo_away = fig.add_axes([0.75, 0.88, 0.1, 0.1], anchor='NE', zorder=1)
        ax_logo_away.imshow(away_team_logo)
        ax_logo_away.axis('off')

        # Save the table plot as PNG
        output_directory = '/home/xxxxxxx/python_scripts/xg_tables'  # Specify your directory here
        os.makedirs(output_directory, exist_ok=True)  # Create directory if it doesn't exist
        table_filename = f'{home_team_name}_vs_{away_team_name}_xg_table.png'
        table_filepath = os.path.join(output_directory, table_filename)
        plt.savefig(table_filepath, bbox_inches='tight')

        if plot_shots:
            FIGWIDTH = 16
            FIGHEIGHT = 9
            NROWS = 1
            NCOLS = 2
            SPACE = 0.05
            MAX_GRID = 0.95

            pitch = VerticalPitch(pad_top=3, pad_bottom=-15,
                                  pad_left=-15, pad_right=-15, linewidth=1, half=True,
                                  pitch_color='grass', stripe=True, line_color='white')

            GRID_WIDTH, GRID_HEIGHT = pitch.grid_dimensions(figwidth=FIGWIDTH, figheight=FIGHEIGHT,
                                                            nrows=NROWS, ncols=NCOLS,
                                                            max_grid=MAX_GRID, space=SPACE)
            TITLE_HEIGHT = 0.08
            ENDNOTE_HEIGHT = 0.04

            fig, axs = pitch.grid(figheight=FIGHEIGHT, grid_width=GRID_WIDTH, grid_height=GRID_HEIGHT,
                                  space=SPACE, ncols=NCOLS, nrows=NROWS, title_height=TITLE_HEIGHT,
                                  endnote_height=ENDNOTE_HEIGHT, axis=False)

            # Plot non-goal shots
            pitch.scatter(home_data[home_data['type'] != 'goal'].x_adjusted, 
                          home_data[home_data['type'] != 'goal'].y_adjusted,
                          s=(home_data[home_data['type'] != 'goal'].expectedGoalsValue.astype(float) * 1900) + 100,
                          c='blue', edgecolors='black', marker='o', ax=axs['pitch'][0], label=f'{home_team_name} Shots (Non-Goals)')
    
            pitch.scatter(away_data[away_data['type'] != 'goal'].x_adjusted, 
                          away_data[away_data['type'] != 'goal'].y_adjusted,
                          s=(away_data[away_data['type'] != 'goal'].expectedGoalsValue.astype(float) * 1900) + 100,
                          c='red', edgecolors='black', marker='o', ax=axs['pitch'][1], label=f'{away_team_name} Shots (Non-Goals)')

            # Plot goal shots with different marker
            pitch.scatter(home_data[home_data['type'] == 'goal'].x_adjusted, 
                          home_data[home_data['type'] == 'goal'].y_adjusted,
                          s=(home_data[home_data['type'] == 'goal'].expectedGoalsValue.astype(float) * 1900) + 100,
                          c='orange', edgecolors='black', marker='*', ax=axs['pitch'][0], label=f'{home_team_name} Shots (Goals)')
    
            pitch.scatter(away_data[away_data['type'] == 'goal'].x_adjusted, 
                          away_data[away_data['type'] == 'goal'].y_adjusted,
                          s=(away_data[away_data['type'] == 'goal'].expectedGoalsValue.astype(float) * 1900) + 100,
                          c='grey', edgecolors='black', marker='*', ax=axs['pitch'][1], label=f'{away_team_name} Shots (Goals)')

            fig.suptitle(f'Xg-Shot-Plot: {home_team_name} vs {away_team_name}', fontsize=24)

            # Add team logos to the title
            ax_logo_home = fig.add_axes([0.15, 0.88, 0.1, 0.1], anchor='NE', zorder=1)
            ax_logo_home.imshow(home_team_logo)
            ax_logo_home.axis('off')

            ax_logo_away = fig.add_axes([0.75, 0.88, 0.1, 0.1], anchor='NE', zorder=1)
            ax_logo_away.imshow(away_team_logo)
            ax_logo_away.axis('off')
            
            # Custom legend
            legend_elements = [Line2D([0], [0], marker='o', color='w', label=f'{home_team_name} Shots (Non-Goals)',
                                      markerfacecolor='blue', markersize=10, markeredgecolor='black'),
                               Line2D([0], [0], marker='*', color='w', label=f'{home_team_name} Shots (Goals)',
                                      markerfacecolor='orange', markersize=10, markeredgecolor='black'),
                               Line2D([0], [0], marker='o', color='w', label=f'{away_team_name} Shots (Non-Goals)',
                                      markerfacecolor='red', markersize=10, markeredgecolor='black'),
                               Line2D([0], [0], marker='*', color='w', label=f'{away_team_name} Shots (Goals)',
                                      markerfacecolor='grey', markersize=10, markeredgecolor='black')]

            fig.legend(handles=legend_elements, loc='upper center', fontsize=12, bbox_to_anchor=(0.5, 0.95), ncol=2)

            # Save the shot plot as PNG
            shotplot_directory = '/home/xxxxxx/python_scripts/xg_shotplots'  
            os.makedirs(shotplot_directory, exist_ok=True)  # Create directory if it doesn't exist
            shotplot_filename = f'{home_team_name}_vs_{away_team_name}_xg_shotplot.png'
            shotplot_filepath = os.path.join(shotplot_directory, shotplot_filename)
            plt.savefig(shotplot_filepath, bbox_inches='tight')
            
            if single_event:
                plt.show()

        if show_table:
            plt.show()

        # Trim the table image to the content
        with Image.open(table_filepath) as img:
            img = img.crop(img.getbbox())
            img.save(table_filepath)

    except Exception as e:
        print(f"An error occurred while processing event ID {event_id}: {e}")

# Generate the xG table and shot plot for the selected event(s)
if choice == 'all':
    for event in finished_events:
        print(f"Generating xG table and shot plot for event ID: {event['eventId']}")
        generate_xg_table(event['eventId'], show_table=False, plot_shots=True, single_event=False)
else:
    selected_event_id = input("Enter the event ID you want to generate the xG table and shot plot for: ").strip()
    generate_xg_table(selected_event_id, show_table=True, plot_shots=True, single_event=True)