moneyball 0.0.1.alpha → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0a01481cc7fd30de73d170270013ab6848e86873
4
- data.tar.gz: 9dff51bfbcd2688a676b798f1b42fc0dddcc1367
3
+ metadata.gz: 7f39378b88ae0c5a200beb76783c60706afd65ec
4
+ data.tar.gz: f114082afb54865c8b46754c2660317361c948fc
5
5
  SHA512:
6
- metadata.gz: 58e2e3ff6d953767ca0e236e7101941d2237241b95ca3896d570f2547890f9aa5f9aea1810ef1fc569b43b382b5fdeb2ecb1643acea443666a7f214dc93424b2
7
- data.tar.gz: 6190298eb57b643bbbb7acf35d0910d6940f14d1069d5df29c27767dcbc7db1f29a28f44ae9902c9b1b1856b6c31e2495706be822b425b09b68bd200af8d2665
6
+ metadata.gz: 8c38fede0049073f02ded2636e7a70c86f1d52aa58b0c3e4e4780321c35309675f4321489b531d6308a4280567c455053a7a041f1eea1fe1ff69036d8989617f
7
+ data.tar.gz: 0227f09c11bd500e0b5d25cf70572145b9088b52f6adae422c9161e66a0ef5fe2c8eda50f3867fe8d56b380354bc55afdf7b6e499f0f5ecf797bf30f5e9601d6
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ *.gem
data/.travis.yml CHANGED
@@ -1,3 +1,4 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.2.2
3
+ - 2.2.0
4
+ - ruby-head
File without changes
data/README.md CHANGED
@@ -1,8 +1,11 @@
1
+ [![Code Climate](https://codeclimate.com/github/geoffharcourt/moneyball/badges/gpa.svg)](https://codeclimate.com/github/geoffharcourt/moneyball)
1
2
  # Moneyball
3
+ [![Build Status](https://travis-ci.org/geoffharcourt/moneyball.svg?branch=master)](https://travis-ci.org/geoffharcourt/moneyball)
2
4
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/moneyball`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
5
+ Moneyball parses MLB Gameday's play-by-play information to pull useful
6
+ quantitative and qualitative information out of the data feed. Moneyball can
7
+ determine where a batted ball was hit, the general classification of the batted
8
+ ball, and stat line information from the plate appearance.
6
9
 
7
10
  ## Installation
8
11
 
@@ -22,7 +25,23 @@ Or install it yourself as:
22
25
 
23
26
  ## Usage
24
27
 
25
- TODO: Write usage instructions here
28
+ `Moneyball::Parser` takes a Nokogiri XML node.
29
+
30
+ ```ruby
31
+ raw_xml = '<atbat event="Strikeout" des="Ty Cobb strikes out swinging."></atbat>'
32
+ nokogiri_node = Nokogiri::XML(raw_xml).search("atbat").first
33
+ stats = Moneyball::Parser.new(nokogiri_node).stats
34
+
35
+ stats.slice(:pa, :ab, :h, :k) # => { pa: 1, ab: 1, h: 0, k: 1 }
36
+ ```
37
+
38
+ `Moneyball::BattedBallLocationExtractor` and `Moneyball::BattedBallTypeExtractor` take a string from the play-by-play summary.
39
+
40
+ ```ruby
41
+ summary = "Billy Hamilton hits a fly ball home run (22) to deep center field."
42
+ Moneyball::BattedBallTypeExtractor.new(summary).categorize # => "Line drive"
43
+ Moneyball::BattedBallLocationExtractor.new(summary).categorize # => "CF"
44
+ ```
26
45
 
27
46
  ## Development
28
47
 
@@ -32,8 +51,16 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
32
51
 
33
52
  ## Contributing
34
53
 
54
+ This is a work in progress! I have been using pieces of this code in private projects, but it's time for the saber community to get a crack at improving this code.
55
+
56
+ Any contributions should come with tests (we use RSpec) and a well documented PR. If you have a specific event from the Gameday feed that prompted your pull request, please reference it in your PR.
57
+
58
+ Here's how to contribute!
35
59
  1. Fork it ( https://github.com/[my-github-username]/moneyball/fork )
36
60
  2. Create your feature branch (`git checkout -b my-new-feature`)
37
61
  3. Commit your changes (`git commit -am 'Add some feature'`)
38
62
  4. Push to the branch (`git push origin my-new-feature`)
39
63
  5. Create a new Pull Request
64
+
65
+ ## License
66
+ Moneyball is © 2015 Geoff Harcourt. It is free software, and may be redistributed under the terms specified in the LICENSE file.
data/Rakefile CHANGED
@@ -1 +1,7 @@
1
- require "bundler/gem_tasks"
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ require File.expand_path("../lib/moneyball/version", __FILE__)
4
+
5
+ RSpec::Core::RakeTask.new('spec')
6
+
7
+ task default: :spec
data/lib/moneyball.rb CHANGED
@@ -1,5 +1,9 @@
1
1
  require "nokogiri"
2
+
2
3
  require "moneyball/version"
4
+ require "moneyball/parser"
5
+ require "moneyball/batted_ball_location_extractor"
6
+ require "moneyball/batted_ball_type_extractor"
3
7
 
4
8
  module Moneyball
5
9
  # Your code goes here...
@@ -0,0 +1,72 @@
1
+ module Moneyball
2
+ class BattedBallLocationExtractor
3
+ def initialize(event_description)
4
+ @event_description = event_description
5
+ end
6
+
7
+ def classification
8
+ case
9
+ when event_description.match(deflected_by_regex("first baseman")) then "1B"
10
+ when event_description.match(deflected_by_regex("second baseman")) then "2B"
11
+ when event_description.match(deflected_by_regex("third baseman")) then "3B"
12
+ when event_description.match(deflected_by_regex("shortstop")) then "SS"
13
+ when event_description.match(unassisted_regex("first baseman")) then "1B"
14
+ when event_description.match(unassisted_regex("second baseman")) then "2B"
15
+ when event_description.match(unassisted_regex("third baseman")) then "3B"
16
+ when event_description.match(unassisted_regex("shortstop")) then "SS"
17
+ when event_description.match(infielder_regex("catcher")) then "C"
18
+ when event_description.match(infielder_regex("first baseman")) then "1B"
19
+ when event_description.match(infielder_regex("second baseman")) then "2B"
20
+ when event_description.match(infielder_regex("third baseman")) then "3B"
21
+ when event_description.match(infielder_regex("shortstop")) then "SS"
22
+ when event_description.match(infielder_regex("pitcher")) then "P"
23
+ when event_description.match(outfielder_regex("left")) then "LF"
24
+ when event_description.match(outfielder_regex("center")) then "CF"
25
+ when event_description.match(outfielder_regex("right")) then "RF"
26
+ when event_description.match(flies_into_sacrifice_double_play_regex("left"))
27
+ "LF"
28
+ when event_description.match(flies_into_sacrifice_double_play_regex("center"))
29
+ "CF"
30
+ when event_description.match(flies_into_sacrifice_double_play_regex("right"))
31
+ "RF"
32
+ when event_description.match(/(caught stealing|fan interference|hit by pitch|reaches on catcher interference|strikes out|out on strikes|walks)/)
33
+ nil
34
+ when event_description.match(/(passed ball|wild pitch).+out at/) then nil
35
+ when event_description.match(/picks off/) then nil
36
+ when event_description.match(/\AThrowing error by/) then nil
37
+ when event_description.match(/\A(\w+ challenged([A-Za-z\s\(\)\-\,])+\:)?([A-Za-z]|\s)+(, Jr.)? out at /)
38
+ nil
39
+ when event_description.match(/ (doubles|hits a home run|singles|triples)\.\s+\Z/) then nil
40
+ else
41
+ raise "No match for batted ball location on '#{event_description}'"
42
+ nil
43
+ end
44
+ end
45
+
46
+ protected
47
+
48
+ attr_reader :event_description
49
+
50
+ private
51
+
52
+ def deflected_by_regex(position)
53
+ /deflected by #{position}/
54
+ end
55
+
56
+ def infielder_regex(position)
57
+ /(ball to|bunt(,| to)|fielded by|(F|f)ielding error by|fly to|line drive to|reaches on a (force attempt, )?missed catch error by [A-Za-z\s\.]+, assist to|out( sharply| softly)? ?(,| to)|play,|pop up to|reaches on a(n)? (fielding |throwing )?error by|reaches on a force attempt, throwing error by|bunt\.\s+Throwing error by|pops into a double play in foul territory,) ?#{position}/
58
+ end
59
+
60
+ def outfielder_regex(position)
61
+ /((ground|fly) ball|drive|fielding error|flies into a double play|fly|grand slam( \(\d?\))|lines into a double play|out)( sharply| softly)?(,| by| down the| to) #{position}(-field line)?/
62
+ end
63
+
64
+ def flies_into_sacrifice_double_play_regex(position)
65
+ /flies into a sacrifice double play(,| to) #{position} fielder/
66
+ end
67
+
68
+ def unassisted_regex(position)
69
+ /#{position}[A-Za-z\s]+unassisted/
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,48 @@
1
+ module Moneyball
2
+ class BattedBallTypeExtractor
3
+ def initialize(event_description)
4
+ @event_description = event_description
5
+ end
6
+
7
+ def classification
8
+ case
9
+ when event_description.match(/line(s)? (drive|into|out)/)
10
+ "Line drive"
11
+ when event_description.match(/fl(ies|y) (ball|out)/)
12
+ "Fly ball"
13
+ when event_description.match(/sacrifice fly/)
14
+ "Fly ball"
15
+ when event_description.match(/flies into a/)
16
+ "Fly ball"
17
+ when event_description.match(/ground(s)?( (ball|into|out)|, fielded)/)
18
+ "Ground ball"
19
+ when event_description.match(/fielder's choice/)
20
+ "Ground ball"
21
+ when event_description.match(/((hits|out on) a sacrifice|ground) bunt/)
22
+ "Ground ball"
23
+ when event_description.match(/reaches on a(n)? ((fielding |missed catch |throwing )?error|force attempt)/)
24
+ "Ground ball"
25
+ when event_description.match(/pop(s)? (into a(n)? (unassisted )?double play|(into a force )?out|up)/)
26
+ "Pop up"
27
+ when event_description.match(/(grand slam|home run)/)
28
+ "Fly ball"
29
+ when event_description.match(/(\AThrowing error|catcher interference|caught stealing|fan interference|hit by pitch|strikes out|out on strikes|walks)/)
30
+ nil
31
+ when event_description.match(/(passed ball|wild pitch).+out at/) then nil
32
+ when event_description.match(/picks off/) then nil
33
+ when event_description.match(/\A(\w+ challenged ([A-Za-z]|\s|\(|\)|\-|\,)+\:)?([A-Za-z]|\s)+(, Jr.)? out at /)
34
+ nil
35
+ when event_description.match(/ triples\.\s+\Z/) then nil
36
+ when event_description.match(/ doubles\.\s+\Z/) then nil
37
+ when event_description.match(/ singles\.\s+\Z/) then nil
38
+ else
39
+ raise "No match for batted ball type on '#{event_description}'"
40
+ nil
41
+ end
42
+ end
43
+
44
+ protected
45
+
46
+ attr_reader :event_description
47
+ end
48
+ end
@@ -0,0 +1,214 @@
1
+ module Moneyball
2
+ class Parser
3
+ def initialize(node)
4
+ @node = node
5
+ end
6
+
7
+ def stats
8
+ {
9
+ pa: pa,
10
+ ab: ab,
11
+ h: h,
12
+ h_1b: h_1b,
13
+ h_2b: h_2b,
14
+ h_3b: h_3b,
15
+ hr: hr,
16
+ bb: bb,
17
+ ibb: ibb,
18
+ k: k,
19
+ roe: roe,
20
+ gidp: gidp,
21
+ dp: dp,
22
+ tp: tp,
23
+ hbp: hbp,
24
+ sf: sf,
25
+ sh: sh,
26
+ fc: fc,
27
+ bi: bi,
28
+ ci: ci,
29
+ fi: fi,
30
+ r: r,
31
+ rbi: rbi
32
+ }
33
+ end
34
+
35
+ protected
36
+
37
+ attr_reader :node
38
+
39
+ private
40
+
41
+ def batter_id
42
+ @batter_id ||= node.attribute("batter").value
43
+ end
44
+
45
+ def r
46
+ @r = node.search("runner[id='#{batter_id}'][score='T']").any? ? 1 : 0
47
+ end
48
+
49
+ def rbi
50
+ @r = node.search("runner[rbi='T']").count
51
+ end
52
+
53
+ def event_value
54
+ @event_value ||= node.attribute("event").value
55
+ end
56
+
57
+ def event_matches(regex)
58
+ if event_value.match(regex)
59
+ 1
60
+ else
61
+ 0
62
+ end
63
+ end
64
+
65
+ def any_outcomes_occurred(outcomes)
66
+ if outcomes.any? { |outcome| outcome == 1 }
67
+ 1
68
+ else
69
+ 0
70
+ end
71
+ end
72
+
73
+ def ab
74
+ @ab ||= any_outcomes_occurred([
75
+ h,
76
+ k,
77
+ batted_out,
78
+ double_play,
79
+ tp,
80
+ roe,
81
+ force_out,
82
+ fc,
83
+ bi,
84
+ fi
85
+ ])
86
+ end
87
+
88
+ def bi
89
+ @bi ||= event_matches(/Batter Interference/)
90
+ end
91
+
92
+ def bb
93
+ @bb ||= event_matches(/Walk/)
94
+ end
95
+
96
+ def ibb
97
+ @ibb ||= event_matches(/(I|i)ntent(ional)? (W|w)alk/)
98
+ end
99
+
100
+ def ci
101
+ @ci ||= event_matches(/Catcher Interference/)
102
+ end
103
+
104
+ def gidp
105
+ @gidp ||= event_matches(/((G|g)rounded (I|i)nto|Sacrifice Bunt) DP/)
106
+ end
107
+
108
+ def dp
109
+ @dp ||= any_outcomes_occurred([gidp, double_play, sacrifice_double_play])
110
+ end
111
+
112
+ def tp
113
+ @tp ||= event_matches(/(T|t)riple (P|p)lay/)
114
+ end
115
+
116
+ def sacrifice_double_play
117
+ @sacrifice_double_play ||= event_matches(/Sac (Bunt|Fly) DP/)
118
+ end
119
+
120
+ def double_play
121
+ @double_play ||= event_matches(/((D|d)ouble (P|p)lay| - DP)/)
122
+ end
123
+
124
+ def fc
125
+ @fc ||= event_matches(/Fielders Choice( Out)?/)
126
+ end
127
+
128
+ def fi
129
+ @fi ||= event_matches(/Fan (I|i)nterference/)
130
+
131
+ if @fi
132
+ parse_hit_from_description
133
+ end
134
+
135
+ @fi
136
+ end
137
+
138
+ def h
139
+ @h ||= any_outcomes_occurred([h_1b, h_2b, h_3b, hr])
140
+ end
141
+
142
+ def hbp
143
+ @hbp ||= event_matches(/Hit By Pitch/)
144
+ end
145
+
146
+ def h_1b
147
+ @h_1b ||= event_matches(/(S|s)ingle/)
148
+ end
149
+
150
+ def h_2b
151
+ @h_2b ||= event_matches(/(D|d)ouble\Z/)
152
+ end
153
+
154
+ def h_3b
155
+ @h_3b ||= event_matches(/(T|t)riple?\Z/)
156
+ end
157
+
158
+ def hr
159
+ @hr ||= event_matches(/(H|h)ome (R|r)un/)
160
+ end
161
+
162
+ def k
163
+ @k ||= event_matches(/(S|s)trikeout/)
164
+ end
165
+
166
+ def batted_out
167
+ @batter_out ||= event_matches(/(Fly|Ground|Line|Pop)( )?(O|o)ut|Grounded Into DP/)
168
+ end
169
+
170
+ def force_out
171
+ @force_out ||= event_matches(/Forceout/)
172
+ end
173
+
174
+ def pa
175
+ @pa ||= if !event_value.match(/Runner Out/)
176
+ 1
177
+ else
178
+ 0
179
+ end
180
+ end
181
+
182
+ def roe
183
+ @reached_on_error ||= event_matches(/Field Error/)
184
+ end
185
+
186
+ def sf
187
+ @sf ||= event_matches(/Sac Fly/)
188
+ end
189
+
190
+ def sh
191
+ @sh ||= event_matches(/Sac(rifice)? Bunt/)
192
+ end
193
+
194
+ def parse_hit_from_description
195
+ description = node.attribute("des").value
196
+
197
+ if description.match(/singles/)
198
+ @h = 1
199
+ @h_1b = 1
200
+ elsif description.match(/doubles/)
201
+ @h = 1
202
+ @h_2b = 1
203
+ elsif description.match(/ground-rule double/)
204
+ @h = 1
205
+ @h_2b = 1
206
+ elsif description.match(/triples/)
207
+ @h = 1
208
+ @h_3b = 1
209
+ elsif description.match(/hits a sacrifice bunt/) && description.match(/error/)
210
+ @roe = 1
211
+ end
212
+ end
213
+ end
214
+ end
@@ -1,3 +1,3 @@
1
1
  module Moneyball
2
- VERSION = "0.0.1.alpha"
2
+ VERSION = "0.0.1"
3
3
  end
data/moneyball.gemspec CHANGED
@@ -9,26 +9,18 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Geoff Harcourt"]
10
10
  spec.email = ["geoff.harcourt@gmail.com"]
11
11
 
12
- spec.summary = %q{Parse quantitative information out of baseball play-by-play descriptions.}
13
- spec.description = %q{Moneyball parses the descriptions of plays from Gameday XML and turns the natural language into statistics for research purposes.}
12
+ spec.summary = %q{A parser for Gameday XML play descriptions.}
13
+ spec.description = %q{Moneyball parses Gameday play-by-play descriptions and extracts usable data from plain English summaries of plays.}
14
14
  spec.homepage = "https://github.com/geoffharcourt/moneyball"
15
15
  spec.license = "MIT"
16
16
 
17
- # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
18
- # delete this section to allow pushing this gem to any host.
19
- # if spec.respond_to?(:metadata)
20
- # spec.metadata['allowed_push_host'] = ": set to 'http://mygemserver.com'"
21
- # else
22
- # raise "rubygems 2.0 or newer is required to protect against public gem pushes."
23
- # end
24
-
25
17
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
18
  spec.bindir = "exe"
27
19
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
20
  spec.require_paths = ["lib"]
29
21
 
30
- spec.add_development_dependency "bundler", "~> 1.9"
31
- spec.add_development_dependency "rake", "~> 10.0"
32
-
33
22
  spec.add_dependency "nokogiri"
23
+
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "rspec", "~> 3.2.0"
34
26
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moneyball
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1.alpha
4
+ version: 0.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Geoff Harcourt
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2015-05-14 00:00:00.000000000 Z
11
+ date: 2015-05-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: bundler
14
+ name: nokogiri
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.9'
20
- type: :development
19
+ version: '0'
20
+ type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '1.9'
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -39,21 +39,21 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '10.0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: nokogiri
42
+ name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ">="
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :runtime
47
+ version: 3.2.0
48
+ type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ">="
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '0'
55
- description: Moneyball parses the descriptions of plays from Gameday XML and turns
56
- the natural language into statistics for research purposes.
54
+ version: 3.2.0
55
+ description: Moneyball parses Gameday play-by-play descriptions and extracts usable
56
+ data from plain English summaries of plays.
57
57
  email:
58
58
  - geoff.harcourt@gmail.com
59
59
  executables: []
@@ -65,12 +65,15 @@ files:
65
65
  - ".travis.yml"
66
66
  - CODE_OF_CONDUCT.md
67
67
  - Gemfile
68
- - LICENSE.txt
68
+ - LICENSE
69
69
  - README.md
70
70
  - Rakefile
71
71
  - bin/console
72
72
  - bin/setup
73
73
  - lib/moneyball.rb
74
+ - lib/moneyball/batted_ball_location_extractor.rb
75
+ - lib/moneyball/batted_ball_type_extractor.rb
76
+ - lib/moneyball/parser.rb
74
77
  - lib/moneyball/version.rb
75
78
  - moneyball.gemspec
76
79
  homepage: https://github.com/geoffharcourt/moneyball
@@ -88,13 +91,13 @@ required_ruby_version: !ruby/object:Gem::Requirement
88
91
  version: '0'
89
92
  required_rubygems_version: !ruby/object:Gem::Requirement
90
93
  requirements:
91
- - - ">"
94
+ - - ">="
92
95
  - !ruby/object:Gem::Version
93
- version: 1.3.1
96
+ version: '0'
94
97
  requirements: []
95
98
  rubyforge_project:
96
- rubygems_version: 2.4.6
99
+ rubygems_version: 2.4.7
97
100
  signing_key:
98
101
  specification_version: 4
99
- summary: Parse quantitative information out of baseball play-by-play descriptions.
102
+ summary: A parser for Gameday XML play descriptions.
100
103
  test_files: []