How can I convert this Python code into a SQLAlchemy query?

62 views Asked by At

I'm building a data dashboard for a project where players field a certain position within a game. Players can also field multiple positions in a single game. The dashboard needs the following data:

  • Each player in the Player table
  • The number of games that player has appeared in, regardless of whether they appeared in multiple positions in that game. (i.e. if Fred appears at 3 positions each in 2 games, the query should return 2 not 6)
  • The winning percentage of games in which that player appeared. (i.e. if John appears in 20 games and 11 of them have `outcome="Win", the query should return 0.55)

I wrote some Python code that accomplishes this. It's very slow and requires a number of database queries in a loop. Is it possible to achieve the same output from a single query?

from sqlalchemy import func, select

def get_player_winning_percentages():
    """
    Returns a list of all players who have appeared in a game, along with the
    number of games they've appeared in (regardless of how many times they
    appeared in each game) and the winning percentage of games they've appeared in.
    Output looks like:
    [("John", 20, 0.55), ("Alejandro", 15, 0.75), ("Fred", 13, 0.5), ...]
    """
    # Get all players who have appeared in a game
    players_query = (
        Player.query.join(GameToPlayerPosition)
        .group_by(Player.id)
        .order_by(func.count(Player.id).desc())
    )
    # For each player with an appearance, find the number of games they've appeared in,
    # and the number of winning games they've appeared in.
    data = []
    for player in players_query.all():
        games = Game.query.filter(
            Game.players.any(Player.name == player.name)
        ).count()
        wins = Game.query.filter(
            Game._outcome == "Win",
            Game.players.any(Player.name == player.name)
        ).count()
        data.append({
            "name": player.name, "count": games, "wins": wins
        })
    
    # Find players who haven't appeared in a game and add them to the data
    no_appearances = Player.query.filter(
        ~Player.id.in_(select(players_query.subquery().c.id))
    ).all()
    data += [{"name": player.name, "count": 0, "wins": 0} for player in no_appearances]
    return data

My database tables look like this:

# I'm using sqlalchemy and Flask_SQLAlchemy
from my_flask_app import db
from sqlalchemy.ext.associationproxy import association_proxy

class Game(db.Model):
    """
    Represents a game. Each game can have several players at different positions.
    A single player can appear in a game multiple times at different positions
    (i.e. have multiple GameToPlayerPosition associations).
    """
    id = db.Column(db.Integer, primary_key=True)
    # "Win" or "Loss"
    outcome = db.Column(db.String(10), nullable=False)
    ... # Other fields
    positions = db.relationship(
        "GameToPlayerPosition", back_populates="game", cascade="all, delete"
    )
    players = association_proxy(
        "positions",
        "player",
        creator=lambda player, position: GameToPlayerPosition(player=player, position=position),
    )

class GameToPlayerPosition(db.Model):
    """
    Association table connecting Game to Player. Adds some additional information,
    namely the `position` column shown here.
    """
    game_id = db.Column(db.ForeignKey("game.id"), primary_key=True)
    player_id = db.Column(db.ForeignKey(f"player.id"), primary_key=True)
    position = db.Column(db.String(10), nullable=False)
    ... # Other fields
    game = db.relationship("Game", back_populates="positions")
    player = db.relationship("Player", back_populates="game_positions")

class Player(db.Model):
    """Players in a game."""
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(60), nullable=False, unique=True)
    ... # Other fields
    game_positions = db.relationship(
        "GameToPlayerPosition", back_populates="player", cascade="all, delete"
    )
    games = association_proxy("game_positions", "game")
0

There are 0 answers