bstats 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2ed7bce6b33ac9e017f6c4687fdfc01916ee9548
4
+ data.tar.gz: 013caea171c873a166ba53dc6519ebd9a4215110
5
+ SHA512:
6
+ metadata.gz: f91e8469424b27379b0267fd299463ce3f69e0dd83b70be5f17867abf157464d81fae2573308b0068a0e81d03b1ba17722666254173f03e1734c01ac5b7c37ee
7
+ data.tar.gz: 0e50363c7cab885e707bd236a4bc75d6ab4e548e82fb76a2785ca48e7706ce602d6ac612a00b878bc3a7a1fcc6bb6c321607a4df22e54608aa1a9ec89564525b
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+ gem 'activerecord', '~> 4.1.6'
3
+ gem 'sqlite3', '~> 1.3.9'
4
+
5
+ group :test do
6
+ gem 'rspec'
7
+ gem 'rspec-activemodel-mocks'
8
+ end
@@ -0,0 +1,21 @@
1
+ # ruby-baseball-stats
2
+ A ruby gem that takes a couple CSVs and does a bit of numbers crunching to generate a few statistics.
3
+ ## How To Install
4
+ First, the gem depends on a few other libraries:
5
+ * Active Record
6
+ * SQLite3
7
+ * RSpec (only needed if running the tests from source)
8
+ If you already have those, great, just install the gem and go. If not, there's a Gemfile in the source you can use with Bundler to get all the needed dependencies. Alternately, just install the gems individually, that works too.
9
+ ### Installing The Gem
10
+ Just like any other local gem:
11
+ `sudo gem install bstats-1.0.0.gem`
12
+ ## Running The Script
13
+ It comes pre-loaded with the stats and players needed to determine the stat winners, so after installing you can just run `bstats` from anywhere and see the results.
14
+ ### Loading New Data
15
+ To load in new player and statistical data, run `sudo bstats import` from a directory containing the 'Master-small.csv' and 'Batting-07-12.csv' files.
16
+
17
+ ## TODO List
18
+ * Import needs work:
19
+ ** Move DB out of /Library/Ruby/Gems into user writable space so that 'sudo' isn't needed to write to it
20
+ ** Allow more dynamic command line management, like filenames (OptionParser)
21
+ * Data model addresses immediate requirements only, could be broken out for more granular and performant data analysis
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/bstats.rb'
@@ -0,0 +1,8 @@
1
+ require 'active_record'
2
+ require 'sqlite3'
3
+ I18n.enforce_available_locales = false
4
+ db_file = File.join(File.dirname(__FILE__), 'temp.db')
5
+ ActiveRecord::Base.establish_connection(
6
+ :adapter => 'sqlite3',
7
+ :database => db_file
8
+ )
@@ -0,0 +1,33 @@
1
+ require_relative '../db_config'
2
+
3
+ class CreatePlayerBattingStatsTable < ActiveRecord::Migration
4
+
5
+ def up
6
+ create_table :player_batting_stats do |t|
7
+ t.string :external_id
8
+ t.string :year
9
+ t.string :league
10
+ t.string :team_id
11
+ t.integer :games, :default => 0
12
+ t.integer :at_bats, :default => 0
13
+ t.integer :runs, :default => 0
14
+ t.integer :hits, :default => 0
15
+ t.integer :doubles, :default => 0
16
+ t.integer :triples, :default => 0
17
+ t.integer :home_runs, :default => 0
18
+ t.integer :runs_batted_in, :default => 0
19
+ t.integer :stolen_bases, :default => 0
20
+ t.integer :caught_stealing, :default => 0
21
+ t.decimal :batting_average, :default => 0.0, :precision => 1, :scale => 3
22
+ t.decimal :slugging_percentage, :default => 0.0, :precision => 1, :scale => 3
23
+ end
24
+ end
25
+
26
+ def down
27
+ drop_table :player_batting_stats if table_exists?(:player_batting_stats)
28
+ end
29
+
30
+ def self.table_exists?(name)
31
+ ActiveRecord::Base.connection.tables.include?(name)
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ require_relative '../db_config'
2
+
3
+ class CreatePlayersTable < ActiveRecord::Migration
4
+
5
+ def up
6
+ create_table :players do |t|
7
+ t.string :external_id
8
+ t.integer :birth_year
9
+ t.string :first_name
10
+ t.string :last_name
11
+ end
12
+ end
13
+
14
+ def down
15
+ drop_table :players if table_exists?(:players)
16
+ end
17
+
18
+ def self.table_exists?(name)
19
+ ActiveRecord::Base.connection.tables.include?(name)
20
+ end
21
+ end
Binary file
@@ -0,0 +1,45 @@
1
+ require_relative 'bstats/player'
2
+ require_relative 'bstats/player_batting_stat'
3
+ require_relative 'bstats/data_persistence'
4
+ require_relative 'bstats/number_crunch'
5
+
6
+ begin
7
+ if ARGV[0] == 'import'
8
+ dp = BStats::DataPersistence.new()
9
+ dp.teardown_db()
10
+ dp.initialize_db()
11
+ puts "Importing data..."
12
+ player_file = File.join(Dir.pwd, 'Master-small.csv')
13
+ stats_file = File.join(Dir.pwd, 'Batting-07-12.csv')
14
+ BStats::Player.import_csv(player_file)
15
+ BStats::PlayerBattingStat.import_csv(stats_file)
16
+ end
17
+
18
+ ba_winner = BStats::NumberCrunch.most_improved_batting_average(from_year=2009, to_year=2010)
19
+ puts "Most Improved Batting Average (2009-2010):"
20
+ puts "\tName: #{ba_winner}"
21
+ puts "\tBatting Average Improvement: #{ba_winner.percent_improved}"
22
+
23
+
24
+ puts "\r\n2007 Oakland A's Slugging Percentage:"
25
+ puts "Name\t\t\tSlugging Percentage"
26
+ BStats::NumberCrunch.team_slugging_percentage('OAK', 2007).each do |stat|
27
+ puts "#{stat.player.last_name}, #{stat.player.first_name}\t\t#{'%.3f' % stat.slugging_percentage}"
28
+ end
29
+
30
+ al11 = BStats::NumberCrunch.triple_crown_winner('AL', 2011)
31
+ puts "2011 AL Triple Crown Winner: #{al11}"
32
+ nl11 = BStats::NumberCrunch.triple_crown_winner('NL', 2011)
33
+ puts "2011 NL Triple Crown Winner: #{nl11}"
34
+ al12 = BStats::NumberCrunch.triple_crown_winner('AL', 2012)
35
+ puts "2012 AL Triple Crown Winner: #{al12}"
36
+ nl12 = BStats::NumberCrunch.triple_crown_winner('NL', 2012)
37
+ puts "2012 NL Triple Crown Winner: #{nl12}"
38
+
39
+ rescue Errno::ENOENT => e
40
+ puts "Files needed to import aren't in the directory calling this script. Include 'Master-small.csv' and 'Batting-07-12.csv' in the directory from which you execute this script."
41
+ rescue Exception => e
42
+ puts e.class
43
+ puts e.inspect
44
+ puts e.backtrace
45
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'winner'
2
+
3
+ module BStats
4
+ class BattingAverageWinner < Winner
5
+ attr_writer :percent_improved
6
+
7
+ def percent_improved
8
+ @percent_improved || 0
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ require_relative '../../db/migrate/create_players_table'
2
+ require_relative '../../db/migrate/create_player_batting_stats_table'
3
+
4
+ module BStats
5
+ class DataPersistence
6
+ def initialize_db()
7
+ CreatePlayersTable.migrate(:up)
8
+ CreatePlayerBattingStatsTable.migrate(:up)
9
+ end
10
+
11
+ def teardown_db()
12
+ CreatePlayersTable.migrate(:down)
13
+ CreatePlayerBattingStatsTable.migrate(:down)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,57 @@
1
+ require_relative '../../lib/bstats/player'
2
+ require_relative '../../lib/bstats/player_batting_stat'
3
+ require_relative 'winner'
4
+ require_relative 'batting_average_winner'
5
+
6
+ module BStats
7
+ class NumberCrunch
8
+ def self.most_improved_batting_average(from_year, to_year, min_at_bats=200)
9
+ winner = BattingAverageWinner.new()
10
+ get_grouped_at_bats(from_year, min_at_bats).each do |y1stat|
11
+ get_grouped_at_bats(to_year, min_at_bats).each do | y2stat |
12
+ if ((y2stat.hits_sum.to_f / y2stat.at_bats_sum.to_f) - (y1stat.hits_sum.to_f / y1stat.at_bats_sum.to_f)) > winner.percent_improved
13
+ winner.percent_improved = ((y2stat.hits_sum.to_f / y2stat.at_bats_sum.to_f) - (y1stat.hits_sum.to_f / y1stat.at_bats_sum.to_f)).round(3)
14
+ winner.player = y2stat.player
15
+ end
16
+ end
17
+ end
18
+ return winner
19
+ end
20
+
21
+ def self.team_slugging_percentage(team_id, year)
22
+ PlayerBattingStat.where("team_id = ? AND year = ?", team_id, year)
23
+ end
24
+
25
+ def self.triple_crown_winner(league, year, min_at_bats=400)
26
+ winner = Winner.new()
27
+ btwinner = get_batting_title_winner(league, year, min_at_bats)
28
+ home_run_king = get_home_run_king(league, year, min_at_bats)
29
+ if btwinner.external_id == home_run_king.external_id
30
+ most_rbis = get_most_rbis(league, year, min_at_bats)
31
+ if btwinner.external_id == most_rbis.external_id
32
+ winner.player = btwinner.player
33
+ end
34
+ end
35
+ return winner
36
+ end
37
+
38
+ private
39
+
40
+ def self.get_grouped_at_bats(year, min_at_bats=200)
41
+ PlayerBattingStat.select(:id, :external_id, :year, "SUM(at_bats) as at_bats_sum", "SUM(hits) as hits_sum").where("year = ?", year).group(:external_id).having("sum(at_bats) >= ?", min_at_bats)
42
+ end
43
+
44
+ def self.get_batting_title_winner(league, year, min_at_bats)
45
+ PlayerBattingStat.where("league = ? AND year = ? AND at_bats >= ?", league, year, min_at_bats).order('batting_average DESC').first
46
+ end
47
+
48
+ def self.get_home_run_king(league, year, min_at_bats)
49
+ PlayerBattingStat.where("league = ? AND year = ? AND at_bats >= ?", league, year, min_at_bats).order('home_runs DESC').first
50
+ end
51
+
52
+ def self.get_most_rbis(league, year, min_at_bats)
53
+ PlayerBattingStat.where("league = ? AND year = ? AND at_bats >= ?", league, year, min_at_bats).order('runs_batted_in DESC').first
54
+ end
55
+ end
56
+ end
57
+
@@ -0,0 +1,22 @@
1
+ require_relative '../../db/db_config'
2
+ require 'csv'
3
+
4
+ module BStats
5
+ class Player < ActiveRecord::Base
6
+ has_many :player_batting_stats, :class_name => 'PlayerBattingStat', :foreign_key => 'external_id', :primary_key => 'external_id'
7
+
8
+ validates :external_id, presence: true
9
+ validates :birth_year, numericality: true
10
+
11
+ def self.import_csv(file)
12
+ CSV.foreach(file, headers: true) do |row|
13
+ player = Player.new()
14
+ player.external_id = row['playerID'].to_s
15
+ player.birth_year = row['birthYear'].to_i
16
+ player.first_name = row['nameFirst'].to_s
17
+ player.last_name = row['nameLast'].to_s
18
+ player.save
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,47 @@
1
+ require_relative '../../db/db_config'
2
+ require 'csv'
3
+
4
+ module BStats
5
+ class PlayerBattingStat < ActiveRecord::Base
6
+ belongs_to :player, :class_name => "Player", :foreign_key => 'external_id', :primary_key => 'external_id'
7
+
8
+ validates :external_id, presence: true
9
+ validates :games, :at_bats, :runs, :hits, :doubles, :triples, :home_runs, :runs_batted_in, :stolen_bases, :caught_stealing, numericality: true
10
+
11
+ def self.import_csv(file)
12
+ begin
13
+ CSV.foreach(file, headers: true) do |row|
14
+ stat = PlayerBattingStat.new()
15
+ stat.external_id = row['playerID'].to_s
16
+ stat.year = row['yearID'].to_s
17
+ stat.league = row['league'].to_s
18
+ stat.team_id = row['teamID'].to_s
19
+ stat.games = row['G'].to_i
20
+ stat.at_bats = row['AB'].to_i
21
+ stat.runs = row['R'].to_i
22
+ stat.hits = row['H'].to_i
23
+ stat.doubles = row['2B'].to_i
24
+ stat.triples = row['3B'].to_i
25
+ stat.home_runs = row['HR'].to_i
26
+ stat.runs_batted_in = row['RBI'].to_i
27
+ stat.stolen_bases = row['SB'].to_i
28
+ stat.caught_stealing = row['CS'].to_i
29
+ stat.batting_average = self.calculate_batting_average(stat.hits, stat.at_bats)
30
+ stat.slugging_percentage = self.calculate_slugging_percentage(stat.hits, stat.doubles, stat.triples, stat.home_runs, stat.at_bats)
31
+ stat.save
32
+ end
33
+ rescue CSV::MalformedCSVError => e
34
+ # certain csv's have file endings that the CSV library doesn't like. Windows may or may not have something to do with that
35
+ # TODO: log file ending failure
36
+ end
37
+ end
38
+
39
+ def self.calculate_batting_average(hits, at_bats)
40
+ [0, "0", ""].include?(at_bats) ? 0 : (hits.to_f / at_bats.to_f).round(3)
41
+ end
42
+
43
+ def self.calculate_slugging_percentage(hits, doubles, triples, home_runs, at_bats)
44
+ [0, "0", ""].include?(at_bats) ? 0 : (((hits - doubles - triples - home_runs) + (2 * doubles) + (3 * triples) + (4 * home_runs)).to_f / at_bats).round(3)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,17 @@
1
+ module BStats
2
+ class Winner
3
+ attr_accessor :player
4
+
5
+ def initialize
6
+ @player = nil
7
+ end
8
+
9
+ def awarded?
10
+ [nil, ""].include?(@player) ? false : true
11
+ end
12
+
13
+ def to_s
14
+ awarded? ? "#{@player.last_name}, #{@player.first_name}" : "No winner."
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+ require 'bstats/batting_average_winner'
3
+ require 'bstats/player'
4
+
5
+ module BStats
6
+ describe BattingAverageWinner do
7
+ describe "before being loaded with a player" do
8
+ before do
9
+ @winner = BattingAverageWinner.new
10
+ end
11
+
12
+ it "should return 0 improvement" do
13
+ expect(@winner.percent_improved).to eq(0)
14
+ end
15
+ end
16
+
17
+ describe "after being loaded" do
18
+ before do
19
+ @winner = BattingAverageWinner.new
20
+ @winner.percent_improved = 0.345
21
+ end
22
+
23
+ it "should return the right improvement percentage" do
24
+ expect(@winner.percent_improved).to eq(0.345)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+ require 'bstats/number_crunch'
3
+ require 'bstats/batting_average_winner'
4
+
5
+ module BStats
6
+ describe NumberCrunch do
7
+ describe "calculating batting average" do
8
+ before do
9
+ @stat1 = mock_model(
10
+ PlayerBattingStat, :external_id => "cabremi01", :year => "2011", :league => "AL", :team_id => "DET", :games => 161, :at_bats => 622, :runs => 109, :hits => 205, :doubles => 40, :triples => 0, :home_runs => 44, :runs_batted_in => 139, :stolen_bases => 4, :caught_stealing => 1, :hits_sum => 205, :at_bats_sum => 622
11
+ )
12
+ @stat2 = mock_model(
13
+ PlayerBattingStat, :external_id => "cabremi01", :year => "2012", :league => "AL", :team_id => "DET", :games => 161, :at_bats => 622, :runs => 109, :hits => 205, :doubles => 40, :triples => 0, :home_runs => 44, :runs_batted_in => 139, :stolen_bases => 4, :caught_stealing => 1, :hits_sum => 205, :at_bats_sum => 622
14
+ )
15
+ @stats = [@stat1, @stat2]
16
+ allow(NumberCrunch).to receive(:get_grouped_at_bats).and_return(@stats)
17
+ end
18
+ it "finds the most improved batting average" do
19
+ expect(NumberCrunch.most_improved_batting_average('2007', '2008').percent_improved).to eq(0)
20
+ end
21
+ end
22
+ describe "calculating triple crown winner" do
23
+ before do
24
+ @player = mock_model(Player, :external_id => "cabremi01" )
25
+ @stat2 = mock_model(
26
+ PlayerBattingStat, :external_id => "cabremi01", :year => "2012", :league => "AL", :team_id => "DET", :games => 161, :at_bats => 622, :runs => 109, :hits => 205, :doubles => 40, :triples => 0, :home_runs => 44, :runs_batted_in => 139, :stolen_bases => 4, :caught_stealing => 1, :hits_sum => 205, :at_bats_sum => 622, :player => @player
27
+ )
28
+ allow(NumberCrunch).to receive(:get_batting_title_winner).and_return(@stat2)
29
+ allow(NumberCrunch).to receive(:get_home_run_king).and_return(@stat2)
30
+ allow(NumberCrunch).to receive(:get_most_rbis).and_return(@stat2)
31
+ end
32
+ it "finds the triple crown winner" do
33
+ expect(NumberCrunch.triple_crown_winner("AL", "2012").player.external_id).to eq("cabremi01")
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+ require 'csv'
3
+ require 'bstats/player_batting_stat'
4
+
5
+ module BStats
6
+ describe PlayerBattingStat do
7
+
8
+ describe "Importing a CSV" do
9
+ let(:data) { "abercre01\t2007\tNL\tFLO\t35\t76\t16\t15\t3\t0\t2\t5\t7\t1\rabreubo01\t2012\tAL\tLAA\t8\t24\t1\t5\t3\t0\t0\t5\t0\t0" }
10
+ before do
11
+ allow(CSV).to receive(:foreach).with("file_path", headers: true).and_return(data)
12
+ end
13
+
14
+ it "should read the CSV it is passed" do
15
+ expect(PlayerBattingStat.import_csv("file_path")).to eq(data)
16
+ end
17
+ end
18
+
19
+ describe "calculating stats" do
20
+ it "should calculate the batting average" do
21
+ expect(PlayerBattingStat.calculate_batting_average(3, 9)).to eq(0.333)
22
+ end
23
+ it "should calculate the slugging percentage" do
24
+ expect(PlayerBattingStat.calculate_slugging_percentage(15, 3, 0, 2, 76)).to eq(0.316)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+ require 'csv'
3
+ require 'bstats/player'
4
+
5
+ module BStats
6
+ describe Player do
7
+
8
+ describe "Importing a CSV" do
9
+ let(:data) { "id1\t1900\tfirst\tlast\rid2\t1901\tfirst2\tlast2"}
10
+
11
+ before do
12
+ allow(CSV).to receive(:foreach).with("file_path", headers: true).and_return(data)
13
+ end
14
+
15
+ it "should read the CSV it is passed" do
16
+ expect(Player.import_csv("file_path")).to eq(data)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+ require 'bstats/winner'
3
+ require 'bstats/player'
4
+
5
+ module BStats
6
+ describe Winner do
7
+ describe "before being loaded with a player" do
8
+ before do
9
+ @winner = Winner.new
10
+ end
11
+
12
+ it "should have no player" do
13
+ expect(@winner.player).to eq(nil)
14
+ end
15
+
16
+ it "should not report awarded" do
17
+ expect(@winner.awarded?).to be_falsey
18
+ end
19
+
20
+ it "should read no winner" do
21
+ expect(@winner.to_s).to eq("No winner.")
22
+ end
23
+ end
24
+
25
+ describe "after being loaded with a player" do
26
+ before do
27
+ @player = mock_model(Player, :last_name => "Baseball", :first_name => "Mr" )
28
+ @winner = Winner.new
29
+ @winner.player = @player
30
+ end
31
+
32
+ it "should have a player" do
33
+ expect(@winner.player).to eq(@player)
34
+ end
35
+
36
+ it "should report awarded" do
37
+ expect(@winner.awarded?).to be_truthy
38
+ end
39
+
40
+ it "should read winners name" do
41
+ expect(@winner.to_s).to eq("Baseball, Mr")
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1 @@
1
+ require 'rspec/active_model/mocks'
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bstats
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Troy Stauffer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-10-17 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A ruby gem that takes a couple CSVs and does a bit of numbers crunching
14
+ to generate a few statistics.
15
+ email: troystauffer@gmail.com
16
+ executables:
17
+ - bstats
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - bin/bstats
22
+ - db/db_config.rb
23
+ - db/migrate/create_player_batting_stats_table.rb
24
+ - db/migrate/create_players_table.rb
25
+ - db/temp.db
26
+ - lib/bstats/batting_average_winner.rb
27
+ - lib/bstats/data_persistence.rb
28
+ - lib/bstats/number_crunch.rb
29
+ - lib/bstats/player.rb
30
+ - lib/bstats/player_batting_stat.rb
31
+ - lib/bstats/winner.rb
32
+ - lib/bstats.rb
33
+ - spec/bstats/batting_average_winner_spec.rb
34
+ - spec/bstats/number_crunch_spec.rb
35
+ - spec/bstats/player_batting_stat_spec.rb
36
+ - spec/bstats/player_spec.rb
37
+ - spec/bstats/winner_spec.rb
38
+ - spec/spec_helper.rb
39
+ - Gemfile
40
+ - README.md
41
+ homepage:
42
+ licenses: []
43
+ metadata: {}
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '2.0'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - '>='
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubyforge_project:
60
+ rubygems_version: 2.0.14
61
+ signing_key:
62
+ specification_version: 4
63
+ summary: Baseball Statistics Analysis Tool
64
+ test_files:
65
+ - spec/bstats/batting_average_winner_spec.rb
66
+ - spec/bstats/number_crunch_spec.rb
67
+ - spec/bstats/player_batting_stat_spec.rb
68
+ - spec/bstats/player_spec.rb
69
+ - spec/bstats/winner_spec.rb
70
+ - spec/spec_helper.rb