チーム防御率とかの計算
最近阪神のチーム防御率がぐんぐんと良くなってます。狩野捕手がどんどん伸びているようで、頼もしい限りです。
というわけで、ちょっといろんな軸で切って防御率の計算をしたくなったのでした。
データの記述方法
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(',')}" }
出来たグラフはこんな感じ。
いい感じに下がって来てますね。
破壊的?持続的?
単に私の解釈が間違ってるだけなのかもしれませんが…
「破壊的イノベーション」とは、今までとは違う全く新しい技術が使われている、という意味ではありません。既存の市場と比べて魅力の薄い、新たな市場を開拓するイノベーションを指すのです!
…言い切っちゃった。ほんとはそんなに確信は無いんですけどね;-p
GAE/Jは破壊的イノベーション - ひがやすを blogを読んでから、もやもやしていたんです。いえ、この記事にかぎらず、以前からちょくちょく感じる事がありました。
Amazonのほうは持続的イノベーション、Googleのほうは破壊的イノベーション。
EC2は、過去の技術をそのまま使える(汎用的な仮想化サービス)ので、連続的な技術なのです。
それに対してGAE/Jは、できることをかなり制限して、しかもRDBMSをすててBigTableにのりかえるっていう非連続ぶり。
この記事に限った事ではありません。持続的 / 破壊的イノベーション、と言われる場合に、過去の技術を発展させた技術か、あるいは全く新しい技術か、という意味で書かれる事が多いような気がします。これ以外にも「SSDは磁気的に記憶するのでもなければ、可動部分も持っていない。破壊的技術だ」的な記事を読んだりして感じてたんですが…それって、「イノベーションのジレンマ」に書かれている「持続的イノベーション」「破壊的イノベーション」とは意味が違ってるんじゃないでしょうか。
SSDやGAE/Jが破壊的イノベーションなのかどうか、EC2が持続的イノベーションなのかどうか、についてはまた考えてみたい所ですが…
至極まっとうな経営判断が故に没落してしまう、ていうのが「イノベーションのジレンマ」
ちょっと長くなりますが、まずは「イノベーションのジレンマ」の序章から引用します。
技術と市場構造の破壊的変化に直面し、失敗した大手企業は、数えればきりがない。一見、これらの企業を襲った変化には共通のパターンなどないように思われる。新しい技術が短期間で旧技術を一掃する場合もあれば、移行に何十年もかかる場合もある。新しい技術が複雑で、開発にコストがかかる場合もある。大手企業がすでにだれよりもうまくやっていた事を単純に拡張しただけで、市場を決定する技術になる場合もある。しかし、全てに共通するのは、失敗につながる決定をくだした時点では、そのリーダーは、世界有数の優良企業と広く認められていた
つまり、変化の速度とか、技術的な革新性とか、そういった事とは関係なく、優良企業が突然凋落する、という事がある、と。で、「イノベーションのジレンマ」で書かれた研究というのは
成功している間の意思決定の方法に、のちのち失敗を招くなんらかの要因がある
という「見解を支持している」と書いてあります。
市場の動向を注意深く調査し、システマティックに最も収益率の高そうなイノベーションに投資配分したからこそ、リーダーの地位を失ったのだ。
つまり至極まっとうな経営判断がまずい事もあるって言ってるんですね。この至極まっとうが故に没落してしまう、ていうのが「イノベーションのジレンマ」なんだと。この本の第一部は、この「優良企業」が「リーダーの地位を失った」実例の調査から「破壊的イノベーション」の姿を描き出しています。
「技術」はただのエンジニアリングではない。価値付加のすべて。イノベーションは、その変化。
「技術」の定義は序章にあります。
本書でいう「技術」とは、組織が労働力、資本、原材料、情報を、価値の高い製品やサービスに変えるプロセスを意味する。
つまり、この技術の概念は、エンジニアリングと製造にとどまらず、マーケティング、投資、マネジメントなどのプロセスを包括するものである。
何とも広いですね。
掛け値有りで案件を取って、大量の下請けを投入して、利益をはじき出す、も技術です。
安く請けて、大量の下請けを投入して、工数が肥大したら追加料金を引き出して赤字にしない、も技術です。
小さい案件を取って、オーバーヘッドを最小限にとどめて、少数精鋭で作る、も技術です。
それぞれに応じた技術がある、じゃなくて、それらそのものも技術なんです。
…閑話休題。「イノベーション」の定義も、そのすぐ後にあります。
「イノベーション」とは、これらの技術の変化を意味する。
なんともあっさりしたものです。技術が変化すればすべからくイノベーション。まぁ、この定義でも「技術」がかなり高い視点のものなので、「PHPからJavaに移行した」なんて言っても同様の労働力と情報を投入して同様のサービスを提供する限りは、「イノベーション」にならないんじゃないかとは思いますが…
「破壊的イノベーション」とは「製品の性能を引き下げる効果を持つイノベーション」
さて、「技術」「イノベーション」はわかったので、持続的/破壊的の定義、ですが…序章に
新技術のほとんどは、製品の性能を高めるものである。これを「持続的技術」と呼ぶ。
しかし、時として「破壊的技術」が現れる。これは、少なくとも短期的には、製品の性能を引き下げる効果を持つイノベーションである。
と、まぁここまでで違和感の大部分は説明出来ます。
使われている(エンジニアリングと製造にとどまる意味での)技術が革新的かどうかだけでは、破壊的技術か、持続的技術か、は言えません。まったく革新的な機構を利用する持続的技術もあれば、「大手企業がすでにだれよりもうまくやっていた事を単純に拡張しただけ」の破壊的技術もある、わけですから。
でも、「製品の性能を引き下げる効果を持つイノベーション」は、「破壊的イノベーション」の定義としては弱いような…
こっからは私の解釈の部分が大きいのですが。
第一章HDDの小型化の歴史を追う中で、3.5"までは破壊的技術とされてるんですが3.5" → 2.5"については持続的技術だと書かれてるんです。もちろん(他の小型化と同様に)2.5"のHDDは容量では3.5"に及びません。ただ、他の小型化と同様に)小さく軽いだけです。
「破壊的イノベーション」とは「(既存の)市場の評価」を引き下げる効果をもつイノベーション?
じゃあ、3.5" → 2.5"の小型化とそれ以外の小型化のなにが違うかというと、
その販売対象であるポータブル・コンピュータ市場は、それ以外の重量、耐久性、消費電力、大きさ、と言った特徴を重視した。
つまり、3.5"HDDの顧客が望んでいたから、て言う事なんですね。まぁ、「持続的/破壊的」を区別するのは「性能」といえば「性能」なんですが、性能を評価するのは既存の顧客なんです。既存の顧客が望む方向の変化は、どんな変化であれ持続的イノベーションなんじゃ無いでしょうか。
発泡酒から第三のビール、なんて変化はたぶん持続的イノベーションになるんじゃないかと思いますし、既存の顧客が望むかどうか。既存市場に受け入れられるかどうか。まぁ「性能を高める」といいつつ、そういう事なんだと思います。
破壊的イノベーションは、魅力の薄い新たな市場を開拓する
「破壊的」ていうのは、「世界有数の優良企業と広く認められていた」リーダーを失墜させる事を指してるんだと思っています。なんで足下をすくわれるかというと、既存顧客は使いようが無いって言うし、他に市場も無いし、とまぁ手を出す理由が無い環境で発展するからなんですね。で、手を出すだけの価値が出る頃にはもう大幅に遅れをとってしまっている。この「手を出さない」理由である「既存顧客に評価されない」「利幅の薄い市場を開拓する」ていうのは破壊的技術が破壊的たるための条件だと思います。そうでなければ当然手を付けてるはずなので。
そんな訳で、破壊的技術は「既存の市場と比べて魅力の薄い、新たな市場を開拓するイノベーション」だと私は認識するに至ったのでした。
Groovyラムダ化計画の挫折
おとうさん、ぼくにもYコンビネータがわかりましたよ!を読んで試しているうちに、なぜか意識が明後日へと飛んでしまいました。…なんかこれ、プロパティーチェーンに見えない?
プロパティの連鎖ならgroovyが理解出来ます。理解出来れば実行もできるやん!てことで、
def parrot = λx.x assert parrot(1) == 1 assert parrot(2) == 2
とかGroovyConsoleに打ち込んで、実行出来そうな気がして来たんです。
もちろん「λx」もそのプロパティ「x」も未定義ですが、そこはそれ、propertyMissingメソッドがあります。で、やってみました。
import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; class Lambda { private static ENGINE = new ScriptEngineManager().getEngineByName("groovy"); def arg def propertyMissing(String name) { if (name.startsWith('λ')) { return new Lambda(arg:"$arg, ${name.substring(1)}") } else { ENGINE.eval("{$arg->$name}") } } } def propertyMissing(String name) { if (name.startsWith('λ')) { return new Lambda(arg:name.substring(1)) } else { throw new MissingPropertyException(name, getClass()) } } def parrot = λx.x println (parrot(1))
これで、なんと!ちゃんと1て出力されます。もちろん
println (parrot(2))
なら2て出力されます!
とまぁ、出来たのはここまで。「(λx.λy.x)(0)(1)」なんかは出来なくて、
def TRUE = λx.λy.x def FALSE = λx.λy.y println (TRUE(0,1)) println (FALSE(0,1))
と2引数のクロージャになってしまったり、
λx.x+1
や
λx.λy.x+y
など演算子が入る場合は、無理やりプロパティにするために
println ((λx.'x+1')(1)) println ((λx.λy.'x+y')(1,2))
と文字列で書いてやらないといけません。
うーん、おしい!…けど、まぁクロージャで代用出来るのでいいか、と途中で飽きてしまったのでした。
「Groovyってどんな言語?」がちょっとおしい。
CodeZineにGroovyってどんな言語? JavaプログラマのためのGroovy入門 (1/7):CodeZineて記事が載っていたのですが、いろいろと気になる所が…
文章は読みやすく、7ページも使って結構おいしい所を一通り紹介しているのですが、それだけにちらほらと目につく瑕が残念です。
気になる所を上げてみます。
2ページ目
Groovyは、動的型付けを行う言語です。変数を利用する際、型宣言は不要です(書いてもかまいません)。実行時に値と変数の型がチェックされ、自動的に最適な型として扱われます。
「動的型付けも静的型付けも出来る」と言うべきでしょうね。必要に応じて型宣言を行い、静的型付けも出来るというのはとても重要な特徴だと思うのです。
さらに気になったのは以下の部分。
また明示的に変数の宣言をする際には、「def 変数名」という形で記述することもできます。例えば変数sに"Hello"と設定する場合は、次のような書き方ができます。
String s = "Hello" s = "Hello" def s = "Hello"
実際には「defを省略出来る」のではありません。まぁGroovy - Tutorial 1 - Getting startedにも
You can assign values to variables for later use. Try the following:
x = 1 println x
と書いてあるので、間違いと言い切る事も出来ないのですが…でも正しくない説明です。ここはちょっとややこしくて、ルーズステートメントの扱いについての理解が必要になります。
ルーズステートメントが存在する場合スクリプト自体もクラスとして展開され、
なり、例中の「String s = "Hello"」は静的型付けの変数の定義、「def s = "Hello"」は動的型付けの変数の定義としてそのrunメソッド中に入ります。
じゃあ「s = "Hello"」はどうなるのか。もちろん先に変数が宣言されていればその変数への代入文になるのですが、そうでない場合、存在しない識別子への代入となります。
で、ルーズステートメント中で存在しない識別子が使用された場合、それはスクリプトに対応して生成されるクラスのプロパティとして扱われるんですね。
なので、「String s = "Hello"」や「def s = "Hello"」は変数が定義されるんですが、「s = "Hello"」はプロパティとして定義されます。
プロパティ。他のメソッドからも参照出来ます。なので、
def foo() {println x} def x = 10 foo()
や
def foo = {->println x} def x = 10 foo()
は通りませんが
def foo() {println x} x = 10 foo()
や
def foo = {->println x} x = 10 foo()
は通ったりします。スクリプトがファイルなら、「new ファイル名」でインスタンスが生成出来るんですが、そのrunメソッドを呼び出した後なら外から取得したりも出来ます。
蛇足ですが、
def x = 10 def foo = {->println x} foo()
は通ったりしますが、これは「def foo = {->…}」がメソッド定義ではなく変数を定義してクロージャを代入しているからで、また別の話です。
また、GroovyConsoleを使っている場合defは実行のたびに消えますが
x = 10
を実行した後に
println x
とやると10が表示されたりします。これもプロパティとなっているからこそrun実行を超えてxが生き延びているんですね。
まぁ、こういった細かな差異があるので、ちゃんと理解してないとまずいと思うんですよね。
3ページ目
クラスの定義は、Javaと同様、「class クラス名」の後に続く{}内に記述をします。クラス内にはメソッドとフィールドをおくことができます。これらは「def 名前」といった形で定義をします。例えば、簡単なクラスを作成してみましょう。
class Helo { def msg Helo(s){ msg = s } def sayHelo(){ println msg } }
まず、フィールドも宣言出来ますがこれはプロパティの宣言ですね(Groovy - Groovy Beans参照)。
まぁそれはそれとして、これだとフィールド/プロパティやメソッドの定義はdefでしかできないように読めてしまいます。「必要に応じて動的型付けも静的型付けも選択出来る」点は明記して欲しい所です。プロパティやメソッドの定義こそ静的型付けが使いたくなる所なんです。私の場合、他のファイルからも使用されるようになりだすとメソッド(特に引数)やプロパティを静的型付けにしたくなって来ます。
Javaからの移行を考えると、動的型付けの緩さというのはとても不安になる所ですし、必要に応じて堅くも出来ますよ、というのは重要なんです。
まぁこのサンプルに限って言えばmsgにどんな型のオブジェクトを入れても動作するので問題ないのですが。
ルーズステートメントについては、少し上で書いたようなもうちょっと細かい仕組みに触れて欲しいと思ったんですが…うーん、それを入れると蛇足になる気もします。その辺りの説明が不要な部分のみの紹介にとどめるのが正しいでしょうね。
あと、連想配列はLinkedMapじゃ無くてLinkedHashMapですね。イージーミスでしょうけど、こういった事もちょっと信用出来ないなー、という印象につながっているのかも…
4ページ目
このページで一番気になるのは条件分岐のサンプルです。
num = new Random().nextInt(100) if (num % 2){ println "${num}は、奇数" } else { println "${num}は、偶数" }
「if (num % 2){」は無いと思うんです。ぱっと見、「割り切れるかどうか」の判定に見えませんか?「if (num % 2 == 0) {」として、thenとelseを入れ替えた方が明快ですよね。これではむしろ「if文の中はやっぱりbool型じゃないとね」と示すためのサンプルに見えてしまいます。
サンプルに対して神経質すぎるきらいも有りますが、サンプルは初心者が参考にするコードです。こういった所には気を使って欲しい所です。
nullチェックやリスト/文字列の空チェックをサンプルにするほうが良かったんじゃないかと思うのですが…
for文についても違和感がありました。
構文の基本的な使い方はJavaとはかなり異なります
から始まって詳細にgroovyのfor文の説明がされているのですが、Javaと同じ書き方も出来ますし、for文自体ほとんど使いません。for (value in list) {…}書式もJavaの拡張for文が無かった頃の遺物と言っていいと思います。あんまり詳細に説明すべき物でもないと思うんですよね。さらっとこんなのもあるよ、程度に留めておいて「後述のクロージャが有るのでfor自体ほとんど使わない」とした方が良いんじゃないかと思いました。
6ページ目
Groovy JDKについての説明で
Groovyでは、Javaの標準クラスライブラリに含まれているクラスの多くを標準で利用可能にしています。これのおかげで、多くのクラスをimportなしに利用することができます。また、Groovy独自に追加されているクラスなども多数用意されています。これらは「Groovy JDK」と呼ばれており、Java SEのかなりのクラスがそのまま持ち込まれています。
と書いてあるのですが、「多くを標準で利用可能に」とか「Java SEのかなりのクラスがそのまま持ち込まれています」とか、Javaのクラスの一部しか利用出来ないという誤解を生んでしまいそうです。実際にはあらゆるJavaのクラスが使用可能です。
groovy JDKについて「Groovy独自に追加されているクラス」だというのも誤りですね。Page Redirectionを見てみると「methods added to the JDK to make it more groovy」と有るので、groovy JDKとは「JDK(のクラス)をもっとgroovyにするために追加したメソッド群」と言うべきでしょう。
数学パズルを解いてみる その1
ちょっと前にtwitter上で数学パズルがいくつか紹介されていて、groovyで解いてみたくなりました。
これ好き。問8.「AとBはそれぞれ、1以上1000以下の整数をひとつ考えて紙に書く。Aの考えた数がBのそれよりも大きい確率は?」
「スマリヤンの究極の論理パズル」て本に載ってるそうです。
人間が解く解き方じゃなくて、Aの手とBの手の2重ループでカウントアップしてとかでもなくて、ちょっと賢しげなプログラムで解いてみたい。
要は1~1000の集合A, Bが有って、その直積集合のうちa > bな物の比率を答える訳ですね。
((1..1000) * (1..1000)).findAll{it.a > it.b}.size() / ((1..1000) * (1..1000)).size()
積集合が&で、直積集合なら*かな、と。でも残念ながら*演算なんて出来ません。そこで演算子オーバーライド。
IntRange.metaClass.multiply = { delegate.collect{a->it.collect{b->[a:a, b:b]}}.sum() }
Aの各要素について{(a, b1), (a, b2),…}のリストを作り、そのリストのリストをsum()で{(a1, b1), (a1, b2),…}+{(a2, b1), (a2, b2),…}…とすることで{(a1, b1), (a1, b2),…(a2, b1), (a2, b2),…}と晴れて*演算で直積集合が得られるようになりました。
で、動くコード。
IntRange.metaClass.multiply = { delegate.collect{a->it.collect{b->[a:a, b:b]}}.sum() } ((1..1000) * (1..1000)).findAll{it.a > it.b}.size() / ((1..1000) * (1..1000)).size()
GroovyConsoleで実行すると、
Result: 0.4995
と正しく答えが出ます。ただ…スッゲー重いです。メモリ的にも計算量的にも…GroovyConsoleを素で起動して実行してもOutOfMemoryErrorで落ちるくらいに重いです。
-Xmx512mしてやったらなんとか動きましたが、数十秒返って来ません。
どうもやっぱり1000×1000の総当たりで直積集合を作るのが重いようなので、直積集合を変数に入れて総当たりを1回にしてみます。
IntRange.metaClass.multiply = { delegate.collect{a->it.collect{b->[a:a, b:b]}}.sum() } def productSet = ((1..1000) * (1..1000)) productSet.findAll{it.a > it.b}.size() / productSet.size()
相変わらず重いですが、だいぶましにはなりました。
せっかくここまでしたので、もうちょっといろんな数列で
例えば1~1000の中から奇数を選んで、とかやってみたくなりました。
IntRange.metaClass.multiply = { delegate.collect{a->it.collect{b->[a:a, b:b]}}.sum() } def A = (1..1000).findAll{it % 2 == 1} def B = A def productSet = (A * B) productSet.findAll{it.a > it.b}.size() / productSet.size()
おっと、IntRangeじゃなくなってしまったので*ができません。Aがどんなクラスでも良いように、ちょっと入れ替えて…
def A = (1..1000).findAll{it % 2 == 1} def B = A A.metaClass.multiply = { delegate.collect{a->it.collect{b->[a:a, b:b]}}.sum() } def productSet = (A * B) productSet.findAll{it.a > it.b}.size() / productSet.size()
これで、A, Bの初期化を変えたり最後のfindAllの条件を変えたりすればどんな条件もばっちりです!-)
ベキ集合
とある数学パズルを解く最中にベキ集合を取得するための算出プロパティを書いたので、メモ。
もっともそもそものパズルは設問を誤解していて、ベキ集合では不十分だったんですが…
ArrayList.metaClass.getPowerSet = {-> if (delegate.size == 0) return [[]]; def last = delegate[-1] (delegate[0..<delegate.size-1] as ArrayList).powerSet.with { delegate + delegate.collect{it + last} } }
これで、ArrayListのインスタンスに読み取り専用のpowerSetプロパティ(算出プロパティ)が定義出来ました。
[1,2,4,8].powerSet.each{println "${it.sum(0)}: $it"}
と2の乗数を使って確認してみると、結果は
0: [] 1: [1] 2: [2] 3: [1, 2] 4: [4] 5: [1, 4] 6: [2, 4] 7: [1, 2, 4] 8: [8] 9: [1, 8] 10: [2, 8] 11: [1, 2, 8] 12: [4, 8] 13: [1, 4, 8] 14: [2, 4, 8] 15: [1, 2, 4, 8]
ちゃんとベキ集合になっているようです。
ほんとは集合なんでpowerSetを定義先するのも戻り値もSetの方が良いんですけど、ちょっとSetだと色々めんどくさいんで手を抜きました^^ゞ
それはJavaのパワー
[id:r_ikeda:20090331:oneliner]に、
動的言語なのでこんなのも書ける。
groovy -e '[Long,Integer,Short,Byte].each() { println it.MAX_VALUE }'
と書いてあったのですが、これはJavaでも書けます。
for (Class c: new Class[]{Long.class, Integer.class, Short.class, Byte.class}) { System.out.println(c.getField("MAX_VALUE").get(null)); }
Groovy版の形を整えれば
[Long,Integer,Short,Byte].each { println(it.MAX_VALUE) }
となり、ほぼ同一です。完全に等価にするとすれば、
for (Class c: Arrays.asList(Long.class, Integer.class, Short.class, Byte.class)) { System.out.println(c.getField("MAX_VALUE").get(null)); }
あたりでしょうか。これが出来ること自体はJavaのパワーです。
とはいえ…やっぱりGroovyって簡潔です。
Javaだと実行するためにはさらにClass定義とmainメソッド定義が必要で、さらにimportも要るので
import java.util.Arrays; public class Main { public static void main(String[] args) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { for (Class c: Arrays.asList(Long.class, Integer.class, Short.class, Byte.class)) { System.out.println(c.getField("MAX_VALUE").get(null)); } } }
なんてことになります。throwsなんて手抜きをせずにちゃんとcatchし出すとさらに凄いことに…
この辺はGroovyのパワーですが、動的かどうかはあまり関係なくて、色々積み上げられたシンタックスシュガーが効いてるんですね。
まぁ、土台となってるJava(VM + API)もかなり貢献しているので、評価してやってほしいなー、と思ったのでした。