チーム防御率とかの計算

最近阪神のチーム防御率がぐんぐんと良くなってます。狩野捕手がどんどん伸びているようで、頼もしい限りです。
というわけで、ちょっといろんな軸で切って防御率の計算をしたくなったのでした。

データの記述方法

DSL風味バリバリで、

[
      4/3: [安藤-狩野: 2/7, ウィリアムス-狩野: 0/1, 藤川-狩野: 0/1],
      4/4: [能見-狩野: 3/5, 渡辺-狩野: 0/1, 阿部-岡崎: 0/1, 藤田-岡崎: 0/1, 江草-清水: 0/1],
      4/5: [福原-狩野: 6/5, 渡辺-狩野: 0/1, 江草-狩野: 0/1, アッチソン-狩野: 0/1, ウィリアムス-岡崎: 0/1],
      4/7: [久保-岡崎: 5/6.1, ウィリアムス-岡崎: 4/0.1, 藤田-岡崎: 1/0.1, 阿部-岡崎: 0/0.2, 江草-岡崎: 0/1.1],
…
]

みたいに書けたらかっこいいなぁ…と思ったのですが…

  • 分数クラスを作ってInteger.divideが分数を返す
  • propertyMissingをオーバライドして「安藤」とか「狩野」が選手を返す
    • 選手 - 選手でバッテリー
  • Mapを解析して
    • トップレベルのキーは分数 -> 日付へ変換
    • バッテリーに対する値は自責点/投球回(回.アウト)に展開

とか。…メンドクサすぎです。
まぁ、とりあえず構築だけできれば良いので、ひとまず

[
  "4/3": [[投手: "安藤", 捕手: "狩野", 投球回: 7, 自責点: 2], [投手: "ウィリアムス", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "藤川", 捕手: "狩野", 投球回: 1, 自責点: 0]],
  "4/4": [[投手: "能見", 捕手: "狩野", 投球回: 5, 自責点: 3], [投手: "渡辺", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "阿部", 捕手: "岡崎", 投球回: 1, 自責点: 0], [投手: "藤田", 捕手: "岡崎", 投球回: 1, 自責点: 0], [投手: "江草", 捕手: "清水", 投球回: 1, 自責点: 0]],
  "4/5": [[投手: "福原", 捕手: "狩野", 投球回: 5, 自責点: 6], [投手: "渡辺", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "江草", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "アッチソン", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "ウィリアムス", 捕手: "岡崎", 投球回: 1, 自責点: 0]],
  "4/7": [[投手: "久保", 捕手: "岡崎", 投球回: 6.1, 自責点: 5], [投手: "ウィリアムス", 捕手: "岡崎", 投球回: 0.1, 自責点: 4], [投手: "藤田", 捕手: "岡崎", 投球回: 0.1, 自責点: 1], [投手: "阿部", 捕手: "岡崎", 投球回: 0.2, 自責点: 0], [投手: "江草", 捕手: "岡崎", 投球回: 1.1, 自責点: 0]],
  "4/8": [[投手: "下柳", 捕手: "狩野", 投球回: 6, 自責点: 2], [投手: "渡辺", 捕手: "狩野", 投球回: 2, 自責点: 0], [投手: "阿部", 捕手: "狩野", 投球回: 1, 自責点: 0]],
  "4/9": [[投手: "石川", 捕手: "狩野", 投球回: 7, 自責点: 4], [投手: "江草", 捕手: "岡崎", 投球回: 1, 自責点: 0], [投手: "アッチソン", 捕手: "岡崎", 投球回: 1, 自責点: 0]],
 "4/10": [[投手: "安藤", 捕手: "狩野", 投球回: 5, 自責点: 3], [投手: "江草", 捕手: "狩野", 投球回: 1, 自責点: 1], [投手: "渡辺", 捕手: "狩野", 投球回: 1, 自責点: 1], [投手: "ウィリアムス", 捕手: "狩野", 投球回: 1, 自責点: 0]],
 "4/11": [[投手: "能見", 捕手: "狩野", 投球回: 6.1, 自責点: 4], [投手: "アッチソン", 捕手: "狩野", 投球回: 0.2, 自責点: 0], [投手: "阿部", 捕手: "狩野", 投球回: 1, 自責点: 0]],
 "4/12": [[投手: "福原", 捕手: "狩野", 投球回: 5, 自責点: 3], [投手: "アッチソン", 捕手: "狩野", 投球回: 1, 自責点: 3], [投手: "江草", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "ウィリアムス", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "藤川", 捕手: "狩野", 投球回: 2, 自責点: 0], [投手: "渡辺", 捕手: "狩野", 投球回: 2, 自責点: 0]],
 "4/15": [[投手: "下柳", 捕手: "狩野", 投球回: 6.1, 自責点: 5], [投手: "渡辺", 捕手: "狩野", 投球回: 0.2, 自責点: 0], [投手: "石川", 捕手: "狩野", 投球回: 2, 自責点: 2]],
 "4/16": [[投手: "久保", 捕手: "岡崎", 投球回: 6, 自責点: 2], [投手: "アッチソン", 捕手: "岡崎", 投球回: 2, 自責点: 0], [投手: "藤川", 捕手: "岡崎", 投球回: 1, 自責点: 1]],
 "4/17": [[投手: "安藤", 捕手: "狩野", 投球回: 7, 自責点: 1], [投手: "アッチソン", 捕手: "狩野", 投球回: 0.2, 自責点: 0], [投手: "ウィリアムス", 捕手: "狩野", 投球回: 0.1, 自責点: 0], [投手: "江草", 捕手: "狩野", 投球回: 1, 自責点: 0]],
 "4/18": [[投手: "能見", 捕手: "狩野", 投球回: 6, 自責点: 0], [投手: "渡辺", 捕手: "狩野", 投球回: 0.1, 自責点: 2], [投手: "アッチソン", 捕手: "狩野", 投球回: 0.2, 自責点: 2], [投手: "ウィリアムス", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "江草", 捕手: "狩野", 投球回: 1, 自責点: 0]],
 "4/19": [[投手: "福原", 捕手: "狩野", 投球回: 5.1, 自責点: 4], [投手: "渡辺", 捕手: "狩野", 投球回: 0.2, 自責点: 0], [投手: "阿部", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "江草", 捕手: "狩野", 投球回: 1, 自責点: 0]],
 "4/21": [[投手: "下柳", 捕手: "狩野", 投球回: 6, 自責点: 2], [投手: "渡辺", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "江草", 捕手: "狩野", 投球回: 1, 自責点: 0]],
 "4/22": [[投手: "久保", 捕手: "岡崎", 投球回: 5, 自責点: 3], [投手: "阿部", 捕手: "岡崎", 投球回: 1, 自責点: 2], [投手: "阿部", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "筒井", 捕手: "狩野", 投球回: 1, 自責点: 0]],
 "4/23": [[投手: "安藤", 捕手: "狩野", 投球回: 7, 自責点: 0], [投手: "ウィリアムス", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "アッチソン", 捕手: "狩野", 投球回: 2, 自責点: 0], [投手: "藤川", 捕手: "狩野", 投球回: 2, 自責点: 0]],
 "4/24": [[投手: "能見", 捕手: "狩野", 投球回: 9, 自責点: 0]],
 "4/25": [[投手: "福原", 捕手: "狩野", 投球回: 8, 自責点: 1], [投手: "筒井", 捕手: "狩野", 投球回: 1, 自責点: 0]],
 "4/26": [[投手: "ジェン", 捕手: "狩野", 投球回: 6, 自責点: 0], [投手: "アッチソン", 捕手: "狩野", 投球回: 0.1, 自責点: 2], [投手: "江草", 捕手: "狩野", 投球回: 1.2, 自責点: 0]],
]

の様に同じ構造(日付をキーとしたリストの中に、その日登板したバッテリー毎の成績をMapで書いたもの)を素のgroovyで書いてしまう事にしました。この「各試合ごとの投手-捕手のペア」が最小単位で、こいつを日別, 投手別, 捕手別, とかでMapに入れておけばその軸で集計出来るな、て訳です。
ちなみに、投球回の「5.1」は5回とアウト1つとって降板、という意味で、普通は「5と1/3回」とか言うやつです。1/3と書いちゃうとややこしい少数になっちゃうので少数表記の「5.1」を使う事にしました。

計算方法

防御率は「自責点 * 9 / 投球回数」で求めるのですが、いろんな軸で切りたいので、Compositeパターンを使って集計出来るようにします。
防御率自体は平均を取ったりとかは無意味(投球回数で重み付けをして平均を取れば良いが、それも大変)なので、自責点と投球回数をCompositeの集計対象にして、「自責点 * 9 / 投球回数」は集計軸ごとに計算させるようにしました。
また、投球回数もややこしいんですよね。6回と1/3とか…10進数でも2進数でも無限小数になってしまいます。3進数クラスとか分数クラスとか作っても良かったのですが…ちょっと面倒。
なので、投球回数は1/3単位の「アウト数」で取ることにしました。

てわけで、まずはComponent。

abstract class PitchingStats {
    abstract int getアウト数();
    abstract int get自責点();
    
    def get防御率() {
        if (アウト数 == 0) return null;
        return (自責点 * 3 * 9 / アウト数).setScale(2, BigDecimal.ROUND_HALF_DOWN)
    }
    
    public String toString(){
        if (アウト数 == 0) return "-";
        return "${(int)(アウト数 / 3)}${アウト数 % 3}/3 自責点${自責点} 防御率${防御率}"
    }
}

PitchingStatsは投球成績。「投球成績」クラスにしてたんですが、日本語クラス名ではフィールドの型宣言とかに使えなかったので断念しました。クラス名は日本語が使えない、と思ったほうが良さそうですね。少なくとも、型名では日本語は使えない。

で、これを集約するCompositeも作ります。

class CompositePitchingStats extends PitchingStats {
    @Delegate private final List<PitchingStats> リスト
    
    int getアウト数() {リスト*.アウト数.sum(0)}
    int get自責点() {リスト*.自責点.sum(0)}

    public CompositePitchingStats() {
        this([])
    }
    
    public CompositePitchingStats(List<PitchingStats> list) {
        this.リスト = list
    }
}

addとかしたいので、@Delegate privateでPichingStatsのListを持ってリスト操作は丸ごと転送させてしまいます。これで計算の基本は出来ました。

データ構造

あとは[チーム成績] -* [試合] -* [バッテリー登板]とか[投手] -* [登板]とか[捕手] -* [登板]とかの構造をPichingStats - CompositPichingStatsを使って作って行きます。

とにかくまずは一番細かなデータとなる、1つの試合の中での1つのバッテリーの登板を表すオブジェクトを見てみましょう。

class バッテリー登板 extends PitchingStats {
    def チーム
    def 投手
    def 捕手

    int アウト数
    int 自責点
    
    public void set投手(String 投手名) {
        this.@投手 = チーム.get投手(投手名)
        投手.add(this)
    }
    
    public void set捕手(String 捕手名) {
        this.@捕手 = チーム.get捕手(捕手名)
        捕手.add(this)
    }
    
    /** 6回と1/3の投球(19アウト)を6.1のような表記で設定するためのセッター */
    public void set投球回(BigDecimal 投球回) {
        アウト数 = 投球回.intValue() * 3 + (投球回*10 - 投球回.intValue() * 10)
    }
}

これはリーフになるので、PitchingStatsを継承しました。あとは6.1回、の様な表記法からアウト数に変換するためのセッター(設定専用プロパティ)やStringの投手名、捕手名から対応する選手のオブジェクトをリポジトリであるチームから取得する、というようなデータ表記から実際の登板情報へ変換するための変換セッターがある程度です。
投手、捕手については相互リンクの管理もしていますが、中途半端です。finalにするか、リンク切断の管理もするか、どっちかにした方がいいんですけど…まぁ、1ファイルスクリプトなので大目に見て下さい。


で、試合のオブジェクトはこんな感じ。

class 試合 {
    @Delegate CompositePitchingStats バッテリー登板リスト = new CompositePitchingStats()
    
    Date 日付
    def チーム
    
    void setバッテリー登板リスト(List list) {
        バッテリー登板リスト.addAll(list.collect {
            new バッテリー登板(チーム: チーム, 投手:it.投手, 捕手:it.捕手, 投球回:it.投球回, 自責点:it.自責点)
        })
    }
    
    public PitchingStats get累積成績() {
        new CompositePitchingStats(チーム.全試合.findAll{it.日付 <= 日付})
    }
}

@Delegateじゃなくて単に継承でも良かったかも…ですが。データ上の生Mapのリストからバッテリー登板のリストに変換する「バッテリー登板」セッターがあるのと、その試合終了時点での成績を表す読み取り専用の「累計成績」プロパティがある以外は、1試合分のバッテリー登板を集めたCompositePitchingStatsそのものです。


次は投手/捕手の軸での集計用のオブジェクト。

class 選手 {
    @Delegate final CompositePitchingStats 登板リスト = new CompositePitchingStats()
    String 名前
    int get登板回数() {return 登板リスト.size()}
    
    public String toString() {登板リスト.toString()}
}

こいつも、ほぼ各選手の登板を集めたCompositePitchingStatsです。登板回数は投手の並べ替えで欲しかったので、ゲッタを追加しました。…うーん。これもDelegateじゃなくて継承で良かったかも…


で、最後にチームです。最初@Singletonで作ってたんですが、12球団あるのにチームがSingletonも変なんで、Factoryの役割も持たせて必ずチームに試合や選手が紐づくようにしました。インナークラスを使えたら楽なんですけど、Groovyではインナークラスが使えないので手で対応。ちょっと大変でした。
まぁ、そのチームです。

public class チーム {
    def 名前
    def 全試合 = new CompositePitchingStats()
    def 投手リスト = []
    def 捕手リスト = []
    
    public void set全試合(Map map) {
        def FORMAT = new SimpleDateFormat("y/M/d")
        全試合.addAll(map.collect{key, value->new 試合(チーム:this, 日付: FORMAT.parse("2009/$key"), バッテリー登板リスト:value)})
    }
    
    def get投手(String 投手名) {
        def 投手 = 投手リスト.find{it.名前 == 投手名}
        if (投手 == null) {
            投手リスト << (投手 = new 選手(名前:投手名))
        }
        return 投手
    }
    
    def get捕手(String 捕手名) {
        def 捕手 = 捕手リスト.find{it.名前 == 捕手名}
        if (捕手 == null) {
            捕手リスト << (捕手 = new 選手(名前:捕手名))
        }
        return 捕手
    }
    
    public String toString() {
        "$名前\n|* |*${捕手リスト*.名前.join('|*')}|*全捕手|\n" + 
        投手リスト.sort{[it.アウト数, it.登板回数]}.reverse().collect{投手->
            "|*${投手.名前}|" + 捕手リスト.collect{捕手->
                new CompositePitchingStats(投手.findAll{it.捕手 == 捕手}).toString()
            }.join("|") + "|$${投手.toString()}|"
        }.join("\n") + "\n" +
        "|*全投手|${捕手リスト*.toString().join('|')}|${全試合.toString()}|\n"
    }
}

「全試合」プロパティのセッターを弄って、「データの記述」の所で書いた日付(試合)をキーとした登板リストのMapを渡してやると、各「日付:登板リスト」のエントリごとに試合情報を生成して「全試合」リストを作るようにしています。また、投手および捕手のリポジトリとするために、投手、捕手のそれぞれで名前をキーとして生成(最初に取得した場合)/取得するためのメソッドがあります。
toStringはややこしい所で、全投手 - 全捕手の2次ループを回して投手 - 捕手別の成績を出力しています。たとえば、最初に書いたデータを全試合に設定して作ったこのオブジェクトなら

阪神

狩野 岡崎 清水 全捕手
能見 26回1/3 自責点7 防御率2.39 - - 26回1/3 自責点7 防御率2.39
安藤 26回0/3 自責点6 防御率2.08 - - 26回0/3 自責点6 防御率2.08
福原 23回1/3 自責点14 防御率5.40 - - 23回1/3 自責点14 防御率5.40
下柳 18回1/3 自責点9 防御率4.42 - - 18回1/3 自責点9 防御率4.42
久保 - 17回1/3 自責点10 防御率5.19 - 17回1/3 自責点10 防御率5.19
江草 8回2/3 自責点1 防御率1.04 2回1/3 自責点0 防御率0.00 1回0/3 自責点0 防御率0.00 12回0/3 自責点1 防御率0.75
渡辺 9回2/3 自責点3 防御率2.79 - - 9回2/3 自責点3 防御率2.79
アッチソン 6回1/3 自責点7 防御率9.95 3回0/3 自責点0 防御率0.00 - 9回1/3 自責点7 防御率6.75
石川 9回0/3 自責点6 防御率6.00 - - 9回0/3 自責点6 防御率6.00
ウィリアムス 5回1/3 自責点0 防御率0.00 1回1/3 自責点4 防御率27.00 - 6回2/3 自責点4 防御率5.40
阿部 4回0/3 自責点0 防御率0.00 2回2/3 自責点2 防御率6.75 - 6回2/3 自責点2 防御率2.70
藤川 5回0/3 自責点0 防御率0.00 1回0/3 自責点1 防御率9.00 - 6回0/3 自責点1 防御率1.50
ジェン 6回0/3 自責点0 防御率0.00 - - 6回0/3 自責点0 防御率0.00
筒井 2回0/3 自責点0 防御率0.00 - - 2回0/3 自責点0 防御率0.00
藤田 - 1回1/3 自責点1 防御率6.75 - 1回1/3 自責点1 防御率6.75
全投手 150回0/3 自責点53 防御率3.18 29回0/3 自責点18 防御率5.59 1回0/3 自責点0 防御率0.00 180回0/3 自責点71 防御率3.55

の様な(はてな記法の)表で出力します。

防御率グラフ

もう一つ出したかったのがチーム防御率の推移のグラフ。上の苦労でデータは出来ているので:

new チーム(名前:"阪神", 全試合:[
  "4/3": [[投手: "安藤", 捕手: "狩野", 投球回: 7, 自責点: 2], [投手: "ウィリアムス", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "藤川", 捕手: "狩野", 投球回: 1, 自責点: 0]],
  "4/4": [[投手: "能見", 捕手: "狩野", 投球回: 5, 自責点: 3], [投手: "渡辺", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "阿部", 捕手: "岡崎", 投球回: 1, 自責点: 0], [投手: "藤田", 捕手: "岡崎", 投球回: 1, 自責点: 0], [投手: "江草", 捕手: "清水", 投球回: 1, 自責点: 0]],
  "4/5": [[投手: "福原", 捕手: "狩野", 投球回: 5, 自責点: 6], [投手: "渡辺", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "江草", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "アッチソン", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "ウィリアムス", 捕手: "岡崎", 投球回: 1, 自責点: 0]],
  "4/7": [[投手: "久保", 捕手: "岡崎", 投球回: 6.1, 自責点: 5], [投手: "ウィリアムス", 捕手: "岡崎", 投球回: 0.1, 自責点: 4], [投手: "藤田", 捕手: "岡崎", 投球回: 0.1, 自責点: 1], [投手: "阿部", 捕手: "岡崎", 投球回: 0.2, 自責点: 0], [投手: "江草", 捕手: "岡崎", 投球回: 1.1, 自責点: 0]],
  "4/8": [[投手: "下柳", 捕手: "狩野", 投球回: 6, 自責点: 2], [投手: "渡辺", 捕手: "狩野", 投球回: 2, 自責点: 0], [投手: "阿部", 捕手: "狩野", 投球回: 1, 自責点: 0]],
  "4/9": [[投手: "石川", 捕手: "狩野", 投球回: 7, 自責点: 4], [投手: "江草", 捕手: "岡崎", 投球回: 1, 自責点: 0], [投手: "アッチソン", 捕手: "岡崎", 投球回: 1, 自責点: 0]],
 "4/10": [[投手: "安藤", 捕手: "狩野", 投球回: 5, 自責点: 3], [投手: "江草", 捕手: "狩野", 投球回: 1, 自責点: 1], [投手: "渡辺", 捕手: "狩野", 投球回: 1, 自責点: 1], [投手: "ウィリアムス", 捕手: "狩野", 投球回: 1, 自責点: 0]],
 "4/11": [[投手: "能見", 捕手: "狩野", 投球回: 6.1, 自責点: 4], [投手: "アッチソン", 捕手: "狩野", 投球回: 0.2, 自責点: 0], [投手: "阿部", 捕手: "狩野", 投球回: 1, 自責点: 0]],
 "4/12": [[投手: "福原", 捕手: "狩野", 投球回: 5, 自責点: 3], [投手: "アッチソン", 捕手: "狩野", 投球回: 1, 自責点: 3], [投手: "江草", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "ウィリアムス", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "藤川", 捕手: "狩野", 投球回: 2, 自責点: 0], [投手: "渡辺", 捕手: "狩野", 投球回: 2, 自責点: 0]],
 "4/15": [[投手: "下柳", 捕手: "狩野", 投球回: 6.1, 自責点: 5], [投手: "渡辺", 捕手: "狩野", 投球回: 0.2, 自責点: 0], [投手: "石川", 捕手: "狩野", 投球回: 2, 自責点: 2]],
 "4/16": [[投手: "久保", 捕手: "岡崎", 投球回: 6, 自責点: 2], [投手: "アッチソン", 捕手: "岡崎", 投球回: 2, 自責点: 0], [投手: "藤川", 捕手: "岡崎", 投球回: 1, 自責点: 1]],
 "4/17": [[投手: "安藤", 捕手: "狩野", 投球回: 7, 自責点: 1], [投手: "アッチソン", 捕手: "狩野", 投球回: 0.2, 自責点: 0], [投手: "ウィリアムス", 捕手: "狩野", 投球回: 0.1, 自責点: 0], [投手: "江草", 捕手: "狩野", 投球回: 1, 自責点: 0]],
 "4/18": [[投手: "能見", 捕手: "狩野", 投球回: 6, 自責点: 0], [投手: "渡辺", 捕手: "狩野", 投球回: 0.1, 自責点: 2], [投手: "アッチソン", 捕手: "狩野", 投球回: 0.2, 自責点: 2], [投手: "ウィリアムス", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "江草", 捕手: "狩野", 投球回: 1, 自責点: 0]],
 "4/19": [[投手: "福原", 捕手: "狩野", 投球回: 5.1, 自責点: 4], [投手: "渡辺", 捕手: "狩野", 投球回: 0.2, 自責点: 0], [投手: "阿部", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "江草", 捕手: "狩野", 投球回: 1, 自責点: 0]],
 "4/21": [[投手: "下柳", 捕手: "狩野", 投球回: 6, 自責点: 2], [投手: "渡辺", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "江草", 捕手: "狩野", 投球回: 1, 自責点: 0]],
 "4/22": [[投手: "久保", 捕手: "岡崎", 投球回: 5, 自責点: 3], [投手: "阿部", 捕手: "岡崎", 投球回: 1, 自責点: 2], [投手: "阿部", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "筒井", 捕手: "狩野", 投球回: 1, 自責点: 0]],
 "4/23": [[投手: "安藤", 捕手: "狩野", 投球回: 7, 自責点: 0], [投手: "ウィリアムス", 捕手: "狩野", 投球回: 1, 自責点: 0], [投手: "アッチソン", 捕手: "狩野", 投球回: 2, 自責点: 0], [投手: "藤川", 捕手: "狩野", 投球回: 2, 自責点: 0]],
 "4/24": [[投手: "能見", 捕手: "狩野", 投球回: 9, 自責点: 0]],
 "4/25": [[投手: "福原", 捕手: "狩野", 投球回: 8, 自責点: 1], [投手: "筒井", 捕手: "狩野", 投球回: 1, 自責点: 0]],
 "4/26": [[投手: "ジェン", 捕手: "狩野", 投球回: 6, 自責点: 0], [投手: "アッチソン", 捕手: "狩野", 投球回: 0.1, 自責点: 2], [投手: "江草", 捕手: "狩野", 投球回: 1.2, 自責点: 0]],
]) .with {

後は集計して

    def graphData = [全試合*.防御率, 全試合*.累積成績*.防御率]

GoogleChartに喰わせるだけです。

    def shortFormat = new SimpleDateFormat("M/d")
    def simpleEncode = {values, max->
        def format = ('A'..'Z') + ('a'..'z') + (0..9)
        values.collect {
           (it == null || Double.isNaN(it) || it < 0) ? "_" : format[(int)(it * (format.size()-1) / max)];
        }.join()
    }
    def max = graphData.flatten().max()
    "http://chart.apis.google.com/chart" +
            "?cht=bvg&chbh=5,0,10&chm=D,0000ff,1,0,2&chxt=y&chs=420x200&chco=00ffff,ffffff" + 
            "&chxr=0,0,${max}&chd=s:${graphData.collect{simpleEncode(it, max)}.join(',')}"
}

出来たグラフはこんな感じ。

いい感じに下がって来てますね。