bstats 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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