├── COPYING ├── .gitignore ├── session.vim ├── scripts ├── nflrank ├── nflstats └── extract-docstring ├── nflcmd ├── cmds │ ├── __init__.py │ ├── rank.py │ └── stats.py ├── version.py └── __init__.py ├── longdesc.rst ├── Makefile ├── UNLICENSE ├── setup.py └── README.md /COPYING: -------------------------------------------------------------------------------- 1 | UNLICENSE -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tags 2 | .*.swp 3 | dist 4 | MANIFEST 5 | build 6 | doc 7 | -------------------------------------------------------------------------------- /session.vim: -------------------------------------------------------------------------------- 1 | au BufWritePost *.py silent !ctags -R --languages=python 2 | -------------------------------------------------------------------------------- /scripts/nflrank: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import nflcmd.cmds.rank 4 | nflcmd.cmds.rank.run() 5 | -------------------------------------------------------------------------------- /scripts/nflstats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import nflcmd.cmds.stats 4 | nflcmd.cmds.stats.run() 5 | -------------------------------------------------------------------------------- /nflcmd/cmds/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module nflcmd.cmds contains all of the scripts shipped with nflcmd. 3 | """ 4 | -------------------------------------------------------------------------------- /nflcmd/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.4' 2 | 3 | __pdoc__ = { 4 | '__version__': "The version of the installed nflcmd module.", 5 | } 6 | -------------------------------------------------------------------------------- /longdesc.rst: -------------------------------------------------------------------------------- 1 | Module nflcmd provides functions and types that are useful for building 2 | new commands. For example, this includes formatting specifications for 3 | tables of data, functions for aligning tabular data, and querying nfldb 4 | for aggregate statistics quickly. 5 | -------------------------------------------------------------------------------- /scripts/extract-docstring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import codecs 4 | 5 | docstring = [] 6 | with codecs.open('nflcmd/__init__.py', 'r', 'utf-8') as f: 7 | for i, line in enumerate(f): 8 | if i == 0: 9 | continue 10 | if line.startswith('"""'): 11 | break 12 | docstring.append(line) 13 | print(''.join(docstring)) 14 | 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REMOTE=Geils:~/www/burntsushi.net/public_html/stuff/nflcmd/ 2 | 3 | all: 4 | @echo "Specify a target." 5 | 6 | pypi: docs longdesc.rst 7 | sudo python2 setup.py register sdist bdist_wininst upload 8 | 9 | docs: 10 | pdoc --html --html-dir ./doc --overwrite ./nflcmd 11 | 12 | longdesc.rst: nflcmd/__init__.py docstring 13 | pandoc -f markdown -t rst -o longdesc.rst docstring 14 | rm -f docstring 15 | 16 | docstring: nflcmd/__init__.py 17 | ./scripts/extract-docstring > docstring 18 | 19 | dev-install: 20 | [[ -n "$$VIRTUAL_ENV" ]] || exit 21 | rm -rf ./dist 22 | python setup.py sdist 23 | pip install -U dist/*.tar.gz 24 | 25 | pep8: 26 | pep8-python2 nflcmd/*.py nflcmd/cmds/*.py 27 | pep8-python2 scripts/nfl{rank,stats} 28 | 29 | push: 30 | git push origin master 31 | git push github master 32 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | from distutils.core import setup 3 | from glob import glob 4 | import os.path as path 5 | 6 | # Snippet taken from - http://goo.gl/BnjFzw 7 | # It's to fix a bug for generating a Windows distribution on Linux systems. 8 | # Linux doesn't have access to the "mbcs" encoding. 9 | try: 10 | codecs.lookup('mbcs') 11 | except LookupError: 12 | ascii = codecs.lookup('ascii') 13 | def wrapper(name, enc=ascii): 14 | return {True: enc}.get(name == 'mbcs') 15 | codecs.register(wrapper) 16 | 17 | install_requires = ['nfldb>=0.0.16'] 18 | try: 19 | import argparse 20 | except ImportError: 21 | install_requires.append('argparse') 22 | try: 23 | from collections import OrderedDict 24 | except ImportError: 25 | install_requires.append('ordereddict') 26 | 27 | cwd = path.dirname(__file__) 28 | longdesc = codecs.open(path.join(cwd, 'longdesc.rst'), 'r', 'utf-8').read() 29 | 30 | version = '0.0.0' 31 | with codecs.open(path.join(cwd, 'nflcmd/version.py'), 'r', 'utf-8') as f: 32 | exec(f.read()) 33 | version = __version__ 34 | assert version != '0.0.0' 35 | 36 | docfiles = glob('doc/nflcmd/*.html') + glob('doc/*.pdf') + glob('doc/*.png') 37 | 38 | setup( 39 | name='nflcmd', 40 | author='Andrew Gallant', 41 | author_email='nflcmd@burntsushi.net', 42 | version=version, 43 | license='UNLICENSE', 44 | description='A set of commands for viewing and ranking NFL data.', 45 | long_description=longdesc, 46 | url='https://github.com/BurntSushi/nflcmd', 47 | classifiers=[ 48 | 'License :: Public Domain', 49 | 'Development Status :: 3 - Alpha', 50 | 'Environment :: Console', 51 | 'Intended Audience :: Developers', 52 | 'Intended Audience :: End Users/Desktop', 53 | 'Intended Audience :: Other Audience', 54 | 'Operating System :: OS Independent', 55 | 'Programming Language :: Python :: 2.6', 56 | 'Programming Language :: Python :: 2.7', 57 | 'Topic :: Database', 58 | ], 59 | platforms='ANY', 60 | packages=['nflcmd', 'nflcmd/cmds'], 61 | data_files=[('share/doc/nflcmd', 62 | ['README.md', 'longdesc.rst', 'UNLICENSE'] 63 | ), 64 | ('share/doc/nflcmd/doc', docfiles), 65 | ], 66 | install_requires=install_requires, 67 | scripts=['scripts/nflstats'] 68 | ) 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | nflcmd is a set of command line programs for interacting with NFL data using 2 | [nfldb](https://github.com/BurntSushi/nfldb). This includes, but is not limited 3 | to, viewing game stats, season stats and ranking players by any combination of 4 | statistical category over any duration. 5 | 6 | There are two main goals of this project: 7 | 8 | * The commands should be simple with sane defaults. 9 | * Commands should be reasonably fast. 10 | 11 | 12 | ### Documentation and getting help 13 | 14 | Run any of the commands with the `--help` flag: 15 | 16 | nflstats --help 17 | nflrank --help 18 | 19 | nflcmd has 20 | [some API documentation](http://pdoc.burntsushi.net/nflcmd), but it's mostly 21 | only useful to developers seeking to add more commands. 22 | 23 | If you need any help or have found a bug, please 24 | [open a new issue on nflcmd's issue 25 | tracker](https://github.com/BurntSushi/nflcmd/issues/new) 26 | or join us at our IRC channel `#nflgame` on FreeNode. 27 | 28 | 29 | ### Installation and dependencies 30 | 31 | nflcmd depends only on [nfldb](https://pypi.python.org/nfldb), which includes 32 | having [a PostgreSQL database accessible to 33 | you](https://github.com/BurntSushi/nfldb/wiki/Installation). 34 | 35 | I've only tested nflcmd with Python 2.7 on a Linux system. In theory, nflcmd 36 | should be able to work on Windows and Mac systems as long as you can get 37 | PostgreSQL running. It is **not** Python 3 compatible. 38 | 39 | 40 | ### Examples for `nflstats` 41 | 42 | `nflstats` shows either game or season statistics for a player. 43 | 44 | Show Tom Brady's stats for the current season: 45 | 46 | nflstats tom brady 47 | 48 | Or for all seasons (in nfldb): 49 | 50 | nflstats tom brady --season 51 | 52 | Or only show his stats for the first four weeks of the 2011 season: 53 | 54 | nflstats tom brady --year 2011 --weeks 1-4 55 | 56 | 57 | ### Examples for `nflrank` 58 | 59 | `nflrank` shows player rankings on one or more statistical categories. 60 | 61 | Show the leading touchdown passers for the 2010 season: 62 | 63 | nflrank passing_tds --years 2010 64 | 65 | Or for all seasons from 2009 to the current season: 66 | 67 | nflrank passing_tds --years 2009- 68 | 69 | The same, but restricted to just the first four weeks of each season: 70 | 71 | nflrank passing_tds --years 2009- --weeks 1-4 72 | 73 | Show the rushing leaders for the Patriots in the 2013 season, ranked first by 74 | touchdowns and then by rushing yards: 75 | 76 | nflrank rushing_tds rushing_yds --teams NE 77 | 78 | Show the most targeted receivers of the 2012 postseason: 79 | 80 | nflrank receiving_tar --years 2012 --post 81 | 82 | Or the running backs who can't hold on to the ball in the current season: 83 | 84 | nflrank fumbles_lost --pos RB 85 | 86 | Or the guys who are best at stripping the ball: 87 | 88 | nflrank defense_ffum 89 | 90 | Or the guys who are best at returning interceptions: 91 | 92 | nflrank defense_int_yds defense_int 93 | 94 | -------------------------------------------------------------------------------- /nflcmd/cmds/rank.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | import argparse 3 | from functools import partial 4 | import sys 5 | 6 | import nfldb 7 | 8 | import nflcmd 9 | 10 | 11 | __all__ = ['run'] 12 | 13 | 14 | def eprint(*args, **kwargs): 15 | kwargs['file'] = sys.stderr 16 | print(*args, **kwargs) 17 | 18 | 19 | def run(): 20 | """Runs the `nflrank` command.""" 21 | db = nfldb.connect() 22 | _, cur_year, _ = nfldb.current(db) 23 | 24 | parser = argparse.ArgumentParser( 25 | description='Show NFL player rankings for statistical categories.') 26 | aa = parser.add_argument 27 | aa(dest='categories', metavar='CATEGORY', nargs='+') 28 | aa('--years', type=str, default=str(cur_year), 29 | help='Show rankings only for the inclusive range of years given,\n' 30 | 'e.g., "2010-2011". Other valid examples: "2010", "-2010",\n' 31 | '"2010-".') 32 | aa('--weeks', type=str, default='', 33 | help='Show rankings only for the inclusive range of weeks given,\n' 34 | 'e.g., "4-8". Other valid examples: "4", "-8",\n' 35 | '"4-".') 36 | aa('--pre', action='store_true', 37 | help='When set, only data from the preseason will be used.') 38 | aa('--post', action='store_true', 39 | help='When set, only data from the postseason will be used.') 40 | aa('--pos', type=str, default=[], nargs='+', 41 | help='When set, only show players in the given positions.') 42 | aa('--teams', type=str, default=[], nargs='+', 43 | help='When set, only show players currently on the given teams.') 44 | aa('--limit', type=int, default=10, 45 | help='Restrict the number of results shown.') 46 | args = parser.parse_args() 47 | 48 | for cat in args.categories: 49 | if cat not in nfldb.stat_categories: 50 | eprint("%s is not a valid statistical category.", cat) 51 | sys.exit(1) 52 | 53 | stype = 'Regular' 54 | if args.pre: 55 | stype = 'Preseason' 56 | if args.post: 57 | stype = 'Postseason' 58 | 59 | years = nflcmd.arg_range(args.years, 2009, cur_year) 60 | weeks = nflcmd.arg_range(args.weeks, 1, 17) 61 | 62 | def to_games(agg): 63 | syrs = years[0] if len(years) == 1 else '%d-%d' % (years[0], years[-1]) 64 | qgames = nflcmd.query_games(db, agg.player, years, stype, weeks) 65 | return nflcmd.Games(db, syrs, qgames.as_games(), agg) 66 | 67 | catq = nfldb.QueryOR(db) 68 | for cat in args.categories: 69 | k = cat + '__ne' 70 | catq.play_player(**{k: 0}) 71 | 72 | q = nfldb.Query(db) 73 | q.game(season_year=years, season_type=stype, week=weeks) 74 | q.andalso(catq) 75 | if len(args.pos) > 0: 76 | posq = nfldb.QueryOR(db) 77 | for pos in args.pos: 78 | posq.player(position=nfldb.Enums.player_pos[pos]) 79 | q.andalso(posq) 80 | if len(args.teams) > 0: 81 | q.player(team=args.teams) 82 | q.sort([(cat, 'desc') for cat in args.categories]) 83 | q.limit(args.limit) 84 | pstats = map(to_games, q.as_aggregate()) 85 | 86 | spec = ['name', 'team', 'game_count'] + args.categories 87 | rows = [nflcmd.header_row(spec)] 88 | rows += map(partial(nflcmd.pstat_to_row, spec), pstats) 89 | print(nflcmd.table(rows)) 90 | -------------------------------------------------------------------------------- /nflcmd/cmds/stats.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | import argparse 3 | from functools import partial 4 | import sys 5 | 6 | import nfldb 7 | 8 | import nflcmd 9 | 10 | 11 | __all__ = ['run'] 12 | 13 | prefix_game = ['week', 'outcome', 'game_date', 'opp'] 14 | 15 | prefix_season = ['year', 'teams', 'game_count'] 16 | 17 | 18 | def eprint(*args, **kwargs): 19 | kwargs['file'] = sys.stderr 20 | print(*args, **kwargs) 21 | 22 | 23 | def show_game_table(db, player, year, stype, week_range=None, pos=None): 24 | if pos is None: 25 | pos = player.position 26 | 27 | games = nflcmd.query_games(db, player, year, stype, week_range).as_games() 28 | pstats = map(partial(nflcmd.Game.make, db, player), games) 29 | 30 | spec = prefix_game + nflcmd.columns['game'][nflcmd.pcolumns[pos]] 31 | rows = [nflcmd.header_row(spec)] 32 | rows += map(partial(nflcmd.pstat_to_row, spec), pstats) 33 | if len(pstats) > 1: 34 | summary = nfldb.aggregate(pstat._pstat for pstat in pstats)[0] 35 | allrows = nflcmd.Game(db, None, '-', summary) 36 | allrows._fgs = [] 37 | for pstat in pstats: 38 | allrows._fgs += pstat.fgs 39 | rows.append(nflcmd.pstat_to_row(spec, allrows)) 40 | print(nflcmd.table(rows)) 41 | 42 | 43 | def show_season_table(db, player, stype, week_range=None, pos=None): 44 | if pos is None: 45 | pos = player.position 46 | _, cur_year, _ = nfldb.current(db) 47 | 48 | pstats = [] 49 | for year in range(2009, cur_year+1): 50 | qgames = nflcmd.query_games(db, player, year, stype, week_range) 51 | games = qgames.as_games() 52 | if len(games) == 0: 53 | continue 54 | 55 | game_stats = map(partial(nflcmd.Game.make, db, player), games) 56 | agg = qgames.sort([]).as_aggregate() 57 | pstats.append(nflcmd.Games(db, year, game_stats, agg[0])) 58 | 59 | spec = prefix_season + nflcmd.columns['season'][nflcmd.pcolumns[pos]] 60 | rows = [nflcmd.header_row(spec)] 61 | rows += map(partial(nflcmd.pstat_to_row, spec), pstats) 62 | if len(pstats) > 1: 63 | summary = nfldb.aggregate(pstat._pstat for pstat in pstats)[0] 64 | allrows = nflcmd.Games(db, '-', [], summary) 65 | allrows._fgs = [] 66 | for pstat in pstats: 67 | allrows._fgs += pstat.fgs 68 | allrows.games += pstat.games 69 | rows.append(nflcmd.pstat_to_row(spec, allrows)) 70 | print(nflcmd.table(rows)) 71 | 72 | 73 | def run(): 74 | """Runs the `nflstats` command.""" 75 | db = nfldb.connect() 76 | _, cur_year, _ = nfldb.current(db) 77 | 78 | parser = argparse.ArgumentParser( 79 | description='Show NFL game stats for a player.') 80 | aa = parser.add_argument 81 | aa(dest='player_query', metavar='PLAYER', nargs='+') 82 | aa('--team', type=str, default=None, 83 | help='Specify the team of the player to help the search.') 84 | aa('--pos', type=str, default=None, 85 | help='Specify the position of the player to help the search.') 86 | aa('--soundex', action='store_true', 87 | help='When set, player names are compared using Soundex instead ' 88 | 'of Levenshtein.') 89 | aa('--year', type=str, default=cur_year, 90 | help='Show game logs for only this year. (Not applicable if ' 91 | '--season is set.)') 92 | aa('--pre', action='store_true', 93 | help='When set, only games from the preseason will be used.') 94 | aa('--post', action='store_true', 95 | help='When set, only games from the postseason will be used.') 96 | aa('--weeks', type=str, default='', 97 | help='Show stats only for the inclusive range of weeks given,\n' 98 | 'e.g., "4-8". Other valid examples: "4", "-8",\n' 99 | '"4-". Has no effect when --season is used.') 100 | aa('--season', action='store_true', 101 | help='When set, statistics are shown by season instead of by game.') 102 | aa('--show-as', type=str, default=None, 103 | help='Force display of player as a particular position. This may need ' 104 | 'to be set for inactive players.') 105 | args = parser.parse_args() 106 | 107 | args.player_query = ' '.join(args.player_query) 108 | player = nflcmd.search(db, args.player_query, args.team, args.pos, 109 | args.soundex) 110 | if player is None: 111 | eprint("Could not find a player given the criteria.") 112 | sys.exit(1) 113 | print('Player matched: %s' % player) 114 | 115 | week_range = nflcmd.arg_range(args.weeks, 1, 17) 116 | stype = 'Regular' 117 | if args.pre: 118 | stype = 'Preseason' 119 | if args.post: 120 | stype = 'Postseason' 121 | 122 | 123 | pos = None 124 | if args.show_as is not None: 125 | pos = nfldb.Enums.player_pos[args.show_as] 126 | elif player.position == nfldb.Enums.player_pos.UNK: 127 | q = nfldb.Query(db) 128 | q.play_player(player_id=player.player_id) 129 | q.sort(('gsis_id', 'desc')) 130 | pos = nfldb.guess_position(q.as_play_players()) 131 | if pos == nfldb.Enums.player_pos.UNK: 132 | eprint("The player matched is not active and I could not guess\n" 133 | "his position. Specify it with the '--show-as' flag.") 134 | sys.exit(1) 135 | print("Guessed position: %s" % pos) 136 | 137 | if args.season: 138 | show_season_table(db, player, stype, week_range, pos) 139 | else: 140 | show_game_table(db, player, args.year, stype, week_range, pos) 141 | -------------------------------------------------------------------------------- /nflcmd/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module nflcmd provides functions and types that are useful for building new 3 | commands. For example, this includes formatting specifications for tables of 4 | data, functions for aligning tabular data, and querying nfldb for aggregate 5 | statistics quickly. 6 | """ 7 | 8 | import nfldb 9 | 10 | columns = { 11 | 'game': { 12 | 'passer': ['passing_cmp', 'passing_att', 'passing_ratio', 13 | 'passing_yds', 'passing_yds_att', 'passing_tds', 14 | 'passing_int', 15 | 'rushing_att', 'rushing_yds', 'rushing_yds_att', 16 | 'rushing_tds', 'fumbles_lost', 17 | ], 18 | 'rusher': ['rushing_att', 'rushing_yds', 'rushing_yds_att', 19 | 'rushing_tds', 20 | 'receiving_rec', 'receiving_tar', 'receiving_yds', 21 | 'receiving_yds_att', 'receiving_tds', 22 | 'fumbles_lost', 23 | 'kickret_yds', 'kickret_tds', 24 | 'puntret_yds', 'puntret_tds', 25 | ], 26 | 'receiver': ['receiving_rec', 'receiving_tar', 'receiving_yds', 27 | 'receiving_yds_att', 'receiving_yac_yds', 'receiving_tds', 28 | 'rushing_att', 'rushing_yds', 'rushing_yds_att', 29 | 'rushing_tds', 30 | 'fumbles_lost', 31 | 'kickret_yds', 'kickret_tds', 32 | 'puntret_yds', 'puntret_tds', 33 | ], 34 | 'kicker': ['fg_0_29', 'fg_30_39', 'fg_40_49', 'fg_50', 35 | 'kicking_fgm', 'kicking_fga', 'fgm_ratio', 36 | 'kicking_xpmade', 'kicking_xpa', 'xpm_ratio', 37 | 'kicking_touchback', 38 | ], 39 | 'punter': ['punting_tot', 'punting_yds', 'punting_touchback', 40 | ], 41 | 'defender': ['defense_tkl', 'defense_ast', 'defense_tkl_tot', 42 | 'defense_sk', 'defense_sk_yds', 43 | 'defense_int', 'defense_int_yds', 'defense_int_tds', 44 | 'defense_frec', 'defense_frec_tds', 'defense_ffum', 45 | 'defense_pass_def', 'defense_safe', 46 | 'kickret_yds', 'kickret_tds', 47 | 'puntret_yds', 'puntret_tds', 48 | ], 49 | }, 50 | 'season': { 51 | 'passer': ['passing_cmp', 'passing_att', 'passing_ratio', 52 | 'passing_yds', 'passing_yds_att', 'passing_yds_game', 53 | 'passing_300', 'passing_tds', 'passing_int', 54 | 'rushing_att', 'rushing_yds', 'rushing_yds_att', 55 | 'rushing_tds', 'fumbles_lost', 56 | ], 57 | 'rusher': ['rushing_att', 'rushing_yds', 'rushing_yds_att', 58 | 'rushing_tds', 59 | 'receiving_rec', 'receiving_tar', 'receiving_yds', 60 | 'receiving_yds_att', 'receiving_tds', 61 | 'fumbles_lost', 62 | 'kickret_yds', 'kickret_tds', 63 | 'puntret_yds', 'puntret_tds', 64 | ], 65 | 'receiver': ['receiving_rec', 'receiving_tar', 'receiving_yds', 66 | 'receiving_yds_att', 'receiving_yac_yds', 'receiving_tds', 67 | 'rushing_att', 'rushing_yds', 'rushing_yds_att', 68 | 'rushing_tds', 69 | 'fumbles_lost', 70 | 'kickret_yds', 'kickret_tds', 71 | 'puntret_yds', 'puntret_tds', 72 | ], 73 | 'kicker': ['fg_0_29', 'fg_30_39', 'fg_40_49', 'fg_50', 74 | 'kicking_fgm', 'kicking_fga', 'fgm_ratio', 75 | 'kicking_xpmade', 'kicking_xpa', 'xpm_ratio', 76 | 'kicking_touchback', 77 | ], 78 | 'punter': ['punting_tot', 'punting_yds', 'punting_touchback', 79 | ], 80 | 'defender': ['defense_tkl', 'defense_ast', 'defense_tkl_tot', 81 | 'defense_sk', 'defense_sk_yds', 82 | 'defense_int', 'defense_int_yds', 'defense_int_tds', 83 | 'defense_frec', 'defense_frec_tds', 'defense_ffum', 84 | 'defense_pass_def', 'defense_safe', 85 | 'kickret_yds', 'kickret_tds', 86 | 'puntret_yds', 'puntret_tds', 87 | ], 88 | }, 89 | } 90 | """ 91 | Specifies the columns to show for game and season logs. 92 | """ 93 | 94 | _epos = nfldb.Enums.player_pos 95 | pcolumns = { 96 | _epos.QB: 'passer', 97 | _epos.RB: 'rusher', _epos.FB: 'rusher', 98 | _epos.WR: 'receiver', _epos.TE: 'receiver', 99 | _epos.K: 'kicker', 100 | _epos.P: 'punter', 101 | } 102 | """ 103 | Maps positions to column list names. 104 | """ 105 | _defense_pos = ['C', 'CB', 'DB', 'DE', 'DL', 'DT', 'FS', 'G', 'ILB', 'LB', 106 | 'LS', 'MLB', 'NT', 'OG', 'OL', 'OLB', 'OT', 'SAF', 'SS', 'T', 107 | ] 108 | for pos in _defense_pos: 109 | pcolumns[_epos[pos]] = 'defender' 110 | 111 | 112 | statfuns = { 113 | 'passing_ratio': lambda p: percent(p.passing_cmp, p.passing_att), 114 | 'passing_yds_att': lambda p: ratio(p.passing_yds, p.passing_att), 115 | 'rushing_yds_att': lambda p: ratio(p.rushing_yds, p.rushing_att), 116 | 'receiving_yds_att': lambda p: ratio(p.receiving_yds, p.receiving_rec), 117 | 'fgm_ratio': lambda p: percent(p.kicking_fgm, p.kicking_fga), 118 | 'xpm_ratio': lambda p: percent(p.kicking_xpmade, p.kicking_xpa), 119 | 'defense_tkl_tot': lambda p: p.defense_tkl + p.defense_ast, 120 | } 121 | """ 122 | A dictionary of derived statistics. Any of the keys in this 123 | dictionary may be used in `nflcmd.columns` and `nflcmd.abbrev`. 124 | """ 125 | 126 | abbrev = { 127 | 'passing_cmp': 'CMP', 'passing_att': 'P Att', 'passing_ratio': '%', 128 | 'passing_yds': 'P Yds', 'passing_yds_att': 'Y/Att', 129 | 'passing_yds_game': 'Y/G', 'passing_300': '300+', 130 | 'passing_tds': 'P TDs', 'passing_int': 'INT', 131 | 'rushing_att': 'R Att', 'rushing_yds': 'R Yds', 'rushing_yds_att': 'Y/Att', 132 | 'rushing_tds': 'R TDs', 133 | 'receiving_tar': 'WR Tar', 'receiving_rec': 'WR Rec', 134 | 'receiving_yds': 'WR Yds', 'receiving_yds_att': 'Y/Att', 135 | 'receiving_yac_yds': 'YAC', 136 | 'receiving_tds': 'WR TDs', 137 | 'fumbles_lost': 'F Lost', 138 | 'kickret_yds': 'KR Yds', 'kickret_tds': 'KR TDs', 139 | 'puntret_yds': 'PR Yds', 'puntret_tds': 'PR TDs', 140 | 'fg_0_29': '0-29 M/A', 'fg_30_39': '30-39 M/A', 141 | 'fg_40_49': '40-49 M/A', 'fg_50': '50+ M/A', 142 | 'kicking_fgm': 'FGM', 'kicking_fga': 'FGA', 'fgm_ratio': '%', 143 | 'kicking_xpmade': 'XPM', 'kicking_xpa': 'XPA', 'xpm_ratio': '%', 144 | 'kicking_touchback': 'TBs', 145 | 'punting_tot': 'Punts', 'punting_yds': 'Yards', 'punting_touchback': 'TBs', 146 | 147 | 'defense_tkl': 'Solo', 'defense_ast': 'Ast', 'defense_tkl_tot': 'Total', 148 | 'defense_sk': 'SK', 'defense_sk_yds': 'SK Yds', 149 | 'defense_int': 'Int', 'defense_int_yds': 'Int Yds', 150 | 'defense_int_tds': 'Int TDs', 151 | 'defense_frec': 'F Rec', 'defense_frec_tds': 'F Rec TDs', 152 | 'defense_ffum': 'F Forced', 153 | 'defense_pass_def': 'Pass def', 'defense_safe': 'Safe', 154 | 155 | # Prefixes for game logs 156 | 'week': 'Week', 'outcome': 'W/L', 'game_date': 'Date', 'opp': 'OPP', 157 | 158 | # Prefixes for season logs 159 | 'year': 'Year', 'teams': 'Team', 'game_count': 'G', 160 | 161 | # Misc. 162 | 'name': 'Player', 'team': 'Team', 163 | } 164 | """ 165 | Abbreviations for statistical fields. (Used in the header of tables.) 166 | """ 167 | 168 | 169 | class Game (object): 170 | """ 171 | Represents a row of player statistics corresponding to a single 172 | game. 173 | """ 174 | @staticmethod 175 | def make(db, player, game): 176 | """ 177 | Create a new `nflcmd.Game` object from `nfldb.Player` and 178 | `nfldb.Game` objects. 179 | """ 180 | pstat = game_stats(db, game, player) 181 | team = player_team_in_game(db, game, player) 182 | return Game(db, game, team, pstat) 183 | 184 | def __init__(self, db, game, team, pstat): 185 | self._db = db 186 | self._game = game 187 | self.team = team 188 | self._pstat = pstat 189 | self._fgs = None 190 | 191 | @property 192 | def fgs(self): 193 | if self._fgs is None: 194 | q = nfldb.Query(self._db) 195 | q.play_player(gsis_id=self.gsis_id, player_id=self.player_id) 196 | q.play_player(kicking_fga=1) 197 | self._fgs = q.as_play_players() 198 | return self._fgs 199 | 200 | @property 201 | def fg_0_29(self): 202 | fgs = fgs_range(self.fgs, 0, 29) 203 | return '%d/%d' % (len(filter(fg_made, fgs)), len(fgs)) 204 | 205 | @property 206 | def fg_30_39(self): 207 | fgs = fgs_range(self.fgs, 30, 39) 208 | return '%d/%d' % (len(filter(fg_made, fgs)), len(fgs)) 209 | 210 | @property 211 | def fg_40_49(self): 212 | fgs = fgs_range(self.fgs, 40, 49) 213 | return '%d/%d' % (len(filter(fg_made, fgs)), len(fgs)) 214 | 215 | @property 216 | def fg_50(self): 217 | fgs = fgs_range(self.fgs, 50, 100) 218 | return '%d/%d' % (len(filter(fg_made, fgs)), len(fgs)) 219 | 220 | @property 221 | def outcome(self): 222 | if self._game is None: 223 | return '-' 224 | return 'W' if self.team == self.winner else 'L' 225 | 226 | @property 227 | def game_date(self): 228 | if self._game is None: 229 | return '-' 230 | return '{d:%b} {d.day}'.format(d=self.start_time) 231 | 232 | @property 233 | def opp(self): 234 | if self._game is None: 235 | return '-' 236 | if self.team == self.away_team: 237 | return '@' + self.home_team 238 | return self.away_team 239 | 240 | def __getattr__(self, k): 241 | try: 242 | return getattr(self._game, k) 243 | except AttributeError: 244 | try: 245 | return getattr(self._pstat, k) 246 | except AttributeError: 247 | return '-' 248 | 249 | 250 | class Games (object): 251 | """ 252 | Represents a row of player statistics corresponding to multiple 253 | games. 254 | """ 255 | def __init__(self, db, year, games, pstat): 256 | self._db = db 257 | self.year = year 258 | self.games = games 259 | self._pstat = pstat 260 | self._fgs = None 261 | 262 | @property 263 | def fgs(self): 264 | if self._fgs is None: 265 | q = nfldb.Query(self._db) 266 | q.play_player(gsis_id=[g.gsis_id for g in self.games]) 267 | q.play_player(player_id=self.player_id, kicking_fga=1) 268 | self._fgs = q.as_play_players() 269 | return self._fgs 270 | 271 | @property 272 | def passing_yds_game(self): 273 | return ratio(self.passing_yds, len(self.games)) 274 | 275 | @property 276 | def name(self): 277 | return self.player.full_name 278 | 279 | @property 280 | def passing_300(self): 281 | return len(filter(lambda p: p.passing_yds >= 300, self.games)) 282 | 283 | @property 284 | def fg_0_29(self): 285 | fgs = fgs_range(self.fgs, 0, 29) 286 | return '%d/%d' % (len(filter(fg_made, fgs)), len(fgs)) 287 | 288 | @property 289 | def fg_30_39(self): 290 | fgs = fgs_range(self.fgs, 30, 39) 291 | return '%d/%d' % (len(filter(fg_made, fgs)), len(fgs)) 292 | 293 | @property 294 | def fg_40_49(self): 295 | fgs = fgs_range(self.fgs, 40, 49) 296 | return '%d/%d' % (len(filter(fg_made, fgs)), len(fgs)) 297 | 298 | @property 299 | def fg_50(self): 300 | fgs = fgs_range(self.fgs, 50, 100) 301 | return '%d/%d' % (len(filter(fg_made, fgs)), len(fgs)) 302 | 303 | @property 304 | def game_count(self): 305 | return len(self.games) 306 | 307 | @property 308 | def team(self): 309 | return self.player.team 310 | 311 | @property 312 | def teams(self): 313 | teams = [] 314 | for g in self.games: 315 | if len(teams) == 0 or teams[-1] != g.team: 316 | teams.append(g.team) 317 | return '/'.join(teams) 318 | 319 | def __getattr__(self, k): 320 | try: 321 | return getattr(self._pstat, k) 322 | except AttributeError: 323 | return '-' 324 | 325 | 326 | def game_stats(db, game, player): 327 | """ 328 | Returns aggregate statistics for a particular `nfldb.Player` in a 329 | single `nfldb.Game`. 330 | """ 331 | q = nfldb.Query(db).game(gsis_id=game.gsis_id) 332 | q.player(player_id=player.player_id) 333 | return q.as_aggregate()[0] 334 | 335 | 336 | def search(db, name, team, pos, soundex=False): 337 | """ 338 | Provides a thin wrapper over nfldb's player search. Namely, if 339 | `name` is one word, then it assumes that it's a first/last name and 340 | tries to match it exactly against the list of results returned from 341 | the Levenshtein/Soundex search. 342 | """ 343 | matches = nfldb.player_search(db, name, team, pos, 344 | limit=100, soundex=soundex) 345 | if len(matches) == 0: 346 | return None 347 | if ' ' not in name: 348 | for p, _ in matches: 349 | if name.lower() in map(str.lower, [p.first_name, p.last_name]): 350 | return p 351 | return matches[0][0] 352 | 353 | 354 | def percent(num, den): 355 | """ 356 | Returns the percentage of integers `num` and `den`. 357 | """ 358 | try: 359 | return 100 * (float(num) / float(den)) 360 | except ZeroDivisionError: 361 | return 0.0 362 | 363 | 364 | def ratio(num, den): 365 | """ 366 | Returns the ratio of integers `num` and `den`. 367 | """ 368 | try: 369 | return float(num) / float(den) 370 | except ZeroDivisionError: 371 | return 0.0 372 | 373 | 374 | def fg_made(p): 375 | """ 376 | Returns `True` if and only if `p` corresponds to a made field goal. 377 | """ 378 | return p.kicking_fgm == 1 379 | 380 | 381 | def fgs_range(fg_plays, start, end): 382 | """ 383 | Given a list of field goal `nfldb.PlayPlayer` objects, return only 384 | the field goals in the range `[start, end]`. 385 | """ 386 | def pred(p): 387 | if p.kicking_fgm == 1: 388 | return start <= p.kicking_fgm_yds <= end 389 | return start <= p.kicking_fgmissed_yds <= end 390 | return filter(pred, fg_plays) 391 | 392 | 393 | def query_games(db, player, year, stype, week_range=None): 394 | """ 395 | Returns a `nfldb.Query` corresponding to a list of games matching 396 | a particular season, season phase and an optional range of weeks. 397 | `player` should be a `nfldb.Player` object. 398 | """ 399 | q = nfldb.Query(db).game(season_year=year, season_type=stype) 400 | if week_range is not None: 401 | q.game(week=week_range) 402 | q.player(player_id=player.player_id) 403 | q.sort(('gsis_id', 'asc')) 404 | return q 405 | 406 | 407 | def player_team_in_game(db, game, player): 408 | """ 409 | Returns the team that the `nfldb.Player` belonged to in a 410 | particular `nfldb.Game`. 411 | """ 412 | q = nfldb.Query(db).game(gsis_id=game.gsis_id) 413 | q.player(player_id=player.player_id) 414 | q.limit(1) 415 | return q.as_play_players()[0].team 416 | 417 | 418 | def pstat_to_row(spec, pstat): 419 | """ 420 | Transforms a player statistic to a list of strings corresponding 421 | to the given spec. Note that `pstat` should be like a 422 | `nfldb.PlayPlayer` object. 423 | """ 424 | row = [] 425 | for column in spec: 426 | v = 0 427 | if column in statfuns: 428 | v = statfuns[column](pstat) 429 | else: 430 | v = getattr(pstat, column) 431 | if isinstance(v, float): 432 | row.append('%0.1f' % v) 433 | else: 434 | row.append(v) 435 | return row 436 | 437 | 438 | def header_row(spec): 439 | """ 440 | Returns a list of strings corresponding to the header row of a 441 | particular `spec`. 442 | """ 443 | header = [] 444 | for column in spec: 445 | header.append(abbrev.get(column, column)) 446 | return header 447 | 448 | 449 | def table(lst): 450 | """ 451 | Takes a list of iterables and returns them as a nicely formatted table. 452 | 453 | All values must be convertible to a str, or else a ValueError will 454 | be raised. 455 | 456 | N.B. I thought Python's standard library had a module that did this 457 | (or maybe it was Go's standard library), but I'm on an airplane and 458 | pydoc sucks. 459 | """ 460 | pad = 2 461 | maxcols = [] 462 | output = [] 463 | first_row = True 464 | for row in lst: 465 | if row is None: 466 | output.append([]) 467 | continue 468 | 469 | output_row = [] 470 | for i, cell in enumerate(row): 471 | cell = str(cell) 472 | if first_row: 473 | maxcols.append(len(cell) + pad) 474 | else: 475 | maxcols[i] = max([maxcols[i], len(cell) + pad]) 476 | output_row.append(cell) 477 | 478 | output.append(output_row) 479 | first_row = False 480 | 481 | rowsep = '-' * sum(maxcols) 482 | nice = [] 483 | for i, row in enumerate(output): 484 | nice_row = [] 485 | for j, cell in enumerate(row): 486 | nice_row.append(cell.rjust(maxcols[j])) 487 | nice.append(''.join(nice_row)) 488 | if i < len(output) - 1: 489 | nice.append(rowsep) 490 | 491 | return '\n'.join(nice) 492 | 493 | def arg_range(arg, lo, hi): 494 | """ 495 | Given a string of the format `[int][-][int]`, return a list of 496 | integers in the inclusive range specified. Open intervals are 497 | allowed, which will be capped at the `lo` and `hi` values given. 498 | 499 | If `arg` is empty or only contains `-`, then all integers in the 500 | range `[lo, hi]` will be returned. 501 | """ 502 | arg = arg.strip() 503 | if len(arg) == 0 or arg == '-': 504 | return range(lo, hi+1) 505 | if '-' not in arg: 506 | return [int(arg)] 507 | start, end = map(str.strip, arg.split('-')) 508 | if len(start) == 0: 509 | return range(lo, int(end)+1) 510 | elif len(end) == 0: 511 | return range(int(start), hi+1) 512 | else: 513 | return range(int(start), int(end)+1) 514 | --------------------------------------------------------------------------------