moneyball 0.0.1.alpha → 0.0.1

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.
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: []