6
\$\begingroup\$

I am working on a stats website for my softball teams that I manage/play for.

Here is my current Stat model:

class Stat(models.Model):
    player = models.ForeignKey(Player)
    game = models.ForeignKey(Game)
    team = models.ForeignKey(Team)
    batting_order = models.IntegerField(default=1)
    plate_apperences = models.IntegerField(default=0, verbose_name='PA')
    runs = models.IntegerField(default=0, verbose_name='R')
    singles = models.IntegerField(default=0, verbose_name='1B')
    doubles = models.IntegerField(default=0, verbose_name='2B')
    triples = models.IntegerField(default=0, verbose_name='3B')
    home_runs = models.IntegerField(default=0, verbose_name='HR')
    runs_batted_in = models.IntegerField(default=0, verbose_name='RBI')
    walks = models.IntegerField(default=0, verbose_name='BB')
    sacrifice_flies = models.IntegerField(default=0, verbose_name='SAC')
    fielders_choice = models.IntegerField(default=0, verbose_name='FC')
    reached_base_on_error = models.IntegerField(default=0, 
        verbose_name='RBOE')

    @property
    def at_bats(self):
        return (self.plate_apperences -
        (self.walks + self.sacrifice_flies + self.reached_base_on_error))

    @property
    def hits(self):
        return self.singles + self.doubles + self.triples + self.home_runs

    @property
    def total_bases(self):
       return (self.plate_apperences -
        (self.walks + self.sacrifice_flies + self.reached_base_on_error))

    @property
    def batting_average(self):
        return round(self.hits / self.at_bats, 3)

    @property
    def on_base_percentage(self):
        if self.at_bats + self.walks + self.sacrifice_flies == 0:
            return 0.000
        return (round(((self.hits + self.walks)
            /(self.at_bats + self.walks + self.sacrifice_flies) ), 3))

    @property
    def slugging_percentage(self):
        return round(self.total_bases / self.at_bats, 3)

    @property
    def runs_created(self):
        return round(self.on_base_percentage + self.total_bases, 0)

    @property
    def total_base_percentage(self):
        return int(round((self.hits + self.walks)
            / (self.at_bats + self.walks), 3))


    def __str__(self):
        if self.game.home:
            return ("{} - {} vs {} - {}.{}".format(
            self.game.date.strftime('%Y/%m/%d'), self.team.name,
            self.game.opponent, self.player.last_name,
            self.player.first_name[0]))
        else:
            return ("{} - {} @ {} - {}.{}".format(
            self.game.date.strftime('%Y/%m/%d'),self.game.team.name,
            self.game.opponent, self.player.last_name,
            self.player.first_name[0]))

    class Meta:
        ordering = ['-game', 'batting_order']

This returns me the data I need... but it is always on a per game basis. What I am looking to do is have 3 options. Stats for: Player, game, and team.

If I select the player button, I will get a player drop down where after selecting a player I will get the players totaled stats per year (50-75 total at bats).

If I select the game button, I should get a team drop down, then a game drop down, then the stats for that game should show all players who played in that game and their stats (talking 3-4 at bats per player)

If I select the team button, I should get a drop down of all my teams, where after selecting one it should show all the players that played for that team (team is comprised of year/team name/week night) and their totaled stats for the year. So, we are talking 10-25 players each with anywhere from 4-75 at bats.

Right now no matter if I select player, game, or team, the stats are individual to a player/game relation. Is there a easy way to query to get a total or do I need a bunch of redundent code to make a TeamStat and PLayerStat api url.

current capture of stats

Again for player I would like a season total by team, not stats by game. So for the image above I would want to see just 1 row of totaled stats (I would need to add a team drop down as well):

Year | Team | PA | AB | R | H | ...
2017 | Brew Berlin | 7 | 6 | 2 | 6 | ...

To help show what I am doing here are my serialzers and urls.

Serializers:

class PlayerSerializer(serializers.ModelSerializer):
    short_name = serializers.CharField(max_length=100)
    long_name = serializers.CharField(max_length=100)

    class Meta:
        model = Player
        fields = '__all__'


class TeamSerializer(serializers.ModelSerializer):
    manager = ManagerSerializer(read_only=True)
    league = LeagueSerializer(read_only=True)
    players = PlayerSerializer(read_only=True, many=True)
    display_name = serializers.CharField(max_length=100)

    class Meta:
        model = Team
        fields = '__all__'


class GameSerializer(serializers.ModelSerializer):
    team = TeamSerializer(read_only=True)

    class Meta:
        model = Game
        fields = '__all__'


class StatSerializer(serializers.ModelSerializer):
    player = PlayerSerializer(read_only=True)
    game = GameSerializer(read_only=True)
    at_bats = serializers.IntegerField()
    hits = serializers.IntegerField()
    total_bases = serializers.IntegerField()
    batting_average = serializers.DecimalField(max_digits=4, 
        decimal_places=3)
    on_base_percentage = serializers.DecimalField(max_digits=4, 
        decimal_places=3)
    slugging_percentage = serializers.DecimalField(max_digits=4, 
        decimal_places=3)
    runs_created = serializers.IntegerField()
    total_base_percentage = serializers.DecimalField(max_digits=4, 
        decimal_places=3)

    class Meta:
        model = Stat
        fields = '__all__'

And here is my current routing:

from rest_framework_nested import routers
from django.conf.urls import url, include

from .api import LeagueViewSet, ManagerViewSet, TeamViewSet, PlayerViewSet
from .api import AwardTypeViewSet, AwardViewSet, GameViewSet, StatViewSet
from .api import GameStatViewSet, PlayerStatViewSet, TeamStatViewSet


#router = DefaultRouter()
router = routers.DefaultRouter()
router.register(r'leagues', LeagueViewSet)
router.register(r'managers', ManagerViewSet)
router.register(r'teams', TeamViewSet, base_name='teams')
router.register(r'players', PlayerViewSet, base_name='players')
router.register(r'awardtypes', AwardTypeViewSet)
router.register(r'games', GameViewSet, base_name='games')
router.register(r'stats', StatViewSet)

team_router = routers.NestedSimpleRouter(router, r'teams', lookup='team')
team_router.register(r'awards', AwardViewSet, base_name='awards')
team_router.register(r'games', GameViewSet, base_name='games')
team_router.register(r'stats', TeamStatViewSet, base_name='stats')
team_router.register(r'players', PlayerViewSet, base_name='players')

game_router = routers.NestedSimpleRouter(router, r'games', lookup='game')
game_router.register(r'stats', GameStatViewSet, base_name='stats')

player_router = routers.NestedSimpleRouter(router, r'players', 
lookup='player')
player_router.register(r'stats', PlayerStatViewSet, base_name='stats')

urlpatterns = [
url(r'^', include(router.urls)),
url(r'^', include(team_router.urls)),
url(r'^', include(game_router.urls)),
url(r'^', include(player_router.urls)),
]

Sorry for the huge post, just trying to give as much helpful info as possible. Thanks for any input.

P.S. This is my first python/django app. I normally code in .net, so if I am way off on my DRF let me know.

\$\endgroup\$
  • \$\begingroup\$ So I have been thinking this through, and by rest standards, my API response is normal. I am thinking all the calculations, meaning summing all the player/game stats into a yearly stat total by player is going to have to be done in my stat controller. Once I get my selectedPlayerID in my watcher, I will have to call a function that takes the API response, create a new object where its attributes are the sum of all the api response attributes. \$\endgroup\$ – Shak3nNotStur3d May 17 '17 at 17:57
0
\$\begingroup\$

So I figured it out... mostly. What I had for my API was correct. I ended up just building the sum and average for stats by year in the angular controller and filter in ng-repeat html tag.

All of the above remains in place and returns stats per player/game, just like the image I posted in the original post. Then in my stat controller I added the following:

$scope.getStatSum = function(stats, property) {
    return stats
    .map(function(x) { return x[property]; })
    .reduce(function(a, b) { return a + b; });
  };

$scope.getStatAverage = function(stats, property) {
    var originalLength = stats.length;
    return $scope.getStatSum(stats, property) / originalLength;
};

These two functions sum or average a property in a list of json data. stats is the list, and property is the string name of the property. To use these functions I need to have a json result list that has already been grouped by an attribute. In my case I need all the stats, normally individual to a player/game relationship, to be grouped by the game.team.year. My html that calls these functions looks like this:

  <tbody>
      <tr ng-repeat="(key, value) in displayedCollection  | groupBy: 'game.team.display_name'">
        <td>{{ key}} </td>
        <td>{{ getStatSum(value, 'plate_apperences') }} </td>
        <td>{{ getStatSum(value, 'at_bats') }} </td>
        <td>{{ getStatSum(value, 'runs') }} </td>
        <td>{{ getStatSum(value, 'hits') }} </td>
        <td>{{ getStatSum(value, 'singles') }} </td>
        <td>{{ getStatSum(value, 'doubles') }} </td>
        <td>{{ getStatSum(value, 'triples') }} </td>
        <td>{{ getStatSum(value, 'home_runs') }} </td>
        <td>{{ getStatSum(value, 'runs_batted_in') }} </td>
        <td>{{ getStatSum(value, 'walks') }} </td>
        <td>{{ getStatSum(value, 'sacrifice_flies') }} </td>
        <td>{{ getStatSum(value, 'fielders_choice') }} </td>
        <td>{{ getStatSum(value, 'reached_base_on_error') }} </td>
        <td>{{ getStatSum(value, 'total_bases') }} </td>
        <td>{{ getStatSum(value, 'runs_created') }} </td>
        <td>{{ getStatAverage(value, 'batting_average') | number: 3 }} </td>
        <td>{{ getStatAverage(value, 'on_base_percentage') | number: 3 }} </td>
        <td>{{ getStatAverage(value, 'slugging_percentage') | number: 3 }} </td>
        <td>{{ getStatAverage(value, 'total_base_percentage') | number: 3 }} </td>

      </tr>
    </tbody>

For my player stats by year this will now return something like this:

enter image description here

The only thing I have left to figure out is why some of the decimal values don't calculate. BA, OBP, SLG, and TBP will sometimes return as 0.000, or NAN. They shouldn't. I will get this worked out, but my original question on how to manipulate the API to return sorted results has been answered... do it on the front end!

\$\endgroup\$
  • \$\begingroup\$ I'm interested in your object design though, wouldn't all the stats be at the player level? And then, if they partook in the game, would their stats be part of the game summary? (similar to team). Apologies if this is an obvious thing, I'm not aware of how stats in baseball are constructed. \$\endgroup\$ – C. Harley Jun 14 '18 at 1:20
  • \$\begingroup\$ Stats are its own entity and has a key to both player and game. Stats can then belinked to a player for season totals, or to an individual game totals. If you took the classic example of a student class and a course class. Students can enroll in courses and the 'grades' are calculated in the enrollment class. In my case, player and game are the student and course, and my stat class is the same as the enrollement class. Where enrollment links students to courses, stats links players to games. I could have called it 'roster' or 'games_played', but all I care about is the calculated stat. \$\endgroup\$ – Shak3nNotStur3d Jun 15 '18 at 5:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.