加重移動平均

移動平均 - uokumuraの日記で作った単純移動平均

List.metaClass.define {
    getAverage {-> sum() / size() }
    
    movingAverage {int n->
        (1..<n).collect {delegate[0..<it].average} +
        (n..size()).collect {i->delegate[(i-n)..<i].average}
    }
}

では過去n件が等しく扱われるため、異常値のn個後まで異常値の影響を受けて高くなり、n+1件目で突如値が変化します。
このため、異常値の後にn件以上平均的な値が続くと、一見何もないところで突然移動平均値が変化したりします。たとえば、次のグラフは2系統の値の棒グラフに対し、n=6の移動平均を線グラフとして出したものですが、

後ろの方にほぼ2で推移しているのに突如移動平均値が下がったり、直前の移動平均値より高い値が出ているにも関わらず移動平均値が下がったりしている部分が有ります。
この不自然な動きを避けるため、直近の値により大きな重み付けをする加重移動平均というものがあるので、これを計算できるよう拡張する事にしました。異常系で無駄に長くなるのもなんなんで、今回は上記の「素直な実装」をベースにします。

リファクタリング

移動平均値の算出の対象になる値は一緒で、その値に対する平均値の算出方法が変わるだけなので、デフォルトではこれまで通りの単純平均のまま、平均の算出方法を必要に応じて平均値の算出方法をクロージャで渡せるようにします。

List.metaClass.define {
    getAverage {-> sum() / size() }
    
    movingAverage {n, average = {it.average}->
        (1..<n).collect{average(delegate[0..<it])} +
        (n..size()).collect {i->average(delegate[(i-n)..<i])}
    }
}

後は、加重平均の算出ロジックをmovingAverageに渡す加重移動平均メソッドを作成するだけです。今回はnから離れるごとに重み付けが1ずつ減っていく線形加重移動平均を作成する事にします。例えばn=3の時には

 [(1*l[0] + 2*l[1] + 3*l[2])/(1+2+3), (1*l[1] + 2*l[2] + 3*l[3])/(1+2+3), …, (1*l[-3] + 2*l[-2] + 3*l[-1])/(1+2+3)]

の様になる加重移動平均です。
これの、移動しつつ平均を出している部分に注目して変形させていくと

(l[0] + l[1] + l[2] + l[1] + l[2] + l[2])/(1+2+3)

[[l[0], l[1], l[2]], [l[1], l[2]], [l[2]]].flatten().average

[l[0..<3], l[1..<3], l[2..<3]].flatten().average

(0..2).collect{ l[it..<l.size()] }.flatten().average

となります。ここまでくれば実装は簡単。

List.metaClass.define {
    getAverage {-> sum() / size() }
    
    movingAverage {n, average = {it.average}->
        (1..<n).collect{average(delegate[0..<it])} +
        (n..size()).collect {i->average(delegate[(i-n)..<i])}
    }
    
    linearWeightedMovingAverage {int n->
        delegate.movingAverage(n) {
            (0..<it.size()).collect{i->it[i..<it.size()]}.flatten().getAverage()
        }
    }
}

…なぜかlinearWeightedMovingAverage中でaverageプロパティが見つけられなかったので、getAverage()と明示的にゲッタを呼んでやる事にしました。
最初のグラフをこの線形加重移動平均で出してみると

単純移動平均のような不自然な変化はなくなりました。ただ、動きが激しくなってます。重み付けの結果最新の値の影響をより大きく受けるようになったんですね。

移動平均

件のチーム防御率のグラフで、初期の成績がいつまでも影響してしまうと狩野捕手の成長が見えないので、移動平均を取りたくなりました。移動平均というのは、リストの先頭から1個ずつ後ろにずらしながらn個のデータの平均値を取ったリストのことで、リストlの移動平均をn=3で取れば、

[(l[0] + l[1] + l[2])/3, (l[1] + l[2] + l[3])/3, (l[2] + l[3] + l[4])/3, …, (l[-3] + l[-2] + l[-1])/3]

の様になります。リストの先頭から1件ずつずらしながらn値の平均を取って行くので移動平均、て訳です。バラツキの有るデータのバラツキを抑制しつつ大きな流れが見れる便利なやつです。
ただ、この移動平均というやつは件数が減ってしまうのが難点です。例えば1〜5まで5件のデータが有るリストからn=3の移動平均を取ると、
[(1+2+3)/3, (2+3+4)/3, (3+4+5)/3]
となるように、平均を取るためにn件要るのでn件目以降からしか移動平均が取れずにn-1件データが減ってしまいます。そうなると元データとグラフを重ねたりするとぶさいくになっちゃうんです。
そこで、今回はn-1件目までは単純にそこまでの平均値を入れて
[(1)/1, (1+2)/2, (1+2+3)/3, (2+3+4)/3, (3+4+5)/3]
と件数をそろえるようにしました。

まずはテスト

1,2,3,…のような1ずつ増える数列を使って、nを奇数にすると移動平均部分が対象データの中央値になって扱いやすいので、IntRangeをテストデータにしました。

assert (1..10).movingAverage(5) == [1,1.5,2,2.5] + (3..8)
assert ((2..20).step(2)).movingAverage(5) == ([2,3,4,5] + (6..16).step(2))
  • 先頭に付加するデータは[1, (1+2)/2, (1+2+3)/3, (1+2+3+4)/4]で[1, 1.5, 2, 2.5]
  • 1..10の移動平均は[(1+2+3+4+5)/5, (2+3+4+5+6)/5, …, (6+7+8+9+10)/5]の中央値のリストを取って3..8

なので、その2つを連結した[1, 1.5, 2, 2.5] + (3..8)で実際の移動平均の算出結果をテストしています。
さらに、その倍のデータで倍の結果が変えるテストもしておきます。

素直な実装

List.metaClass.define {
    getAverage {-> sum() / size() }
    
    movingAverage {int n->
        (1..<n).collect{delegate[0..<it].average} +
        (n..size()).collect {i->delegate[(i-n)..<i].average}
    }
}
  • 1件目からn件目直前まではそこまでの値の平均値のリスト
  • n件目以降はそこまでのn件のデータを平均したリスト

を結合します。というそのままのコードです。

とりあえず動いたので、特殊ケースを

コードを眺めてみると

    movingAverage {int n->
        (1..<n).collect{delegate[0..<it].average} +
        (n..size()).collect {i->delegate[(i-n)..<i].average}
    }

まぁ、空のリストが非常に危なそうですね。ほかに、n=1の場合、n=sizeの場合、n > sizeの場合、が危なそうです。

assert [].movingAverage(3) == []
assert [1,2,3].movingAverage(1) == [1,2,3]
assert [1,2,3].movingAverage(3) == [1,1.5,2]
assert [1,2,3].movingAverage(4) == [1,1.5,2]

まずは0件でエラーが出たので、とりあえずガード節を追加します。

    movingAverage {int n->
        if (empty) {
            return []
        }
        (1..<n).collect{delegate[0..<it].average} +
        (n..size()).collect {i->delegate[(i-n)..<i].average}
    }

今度はn>sizeのケースで発生しました。n=1やn=sizeは大丈夫だったようです。n > sizeの場合、移動平均計算を行うn件目に到達しないので、n=sizeの場合と期待する結果は一緒になります。なので、n>sizeならn=sizeにしてしまいましょう。

    movingAverage {int n->
        if (empty) {
            return []
        }
        if (n > size()) {
            n = size()
        }
        (1..<n).collect{delegate[0..<it].average} +
        (n..size()).collect {i->delegate[(i-n)..<i].average}
    }

最後にエラーケースも

nに0以下の値を渡したらIllegalArgumentExceptionですね。

try {
    [1,2,3].movingAverage(0)
    assert false: "移動平均のnに0は指定できない"
} catch (IllegalArgumentException expected) {
}
try {
    [1,2,3].movingAverage(-1)
    assert false: "移動平均のnに負数は指定できない"
} catch (IllegalArgumentException expected) {
}

ちゃんとassertで捕まえられる事を確認して、実装です。

    movingAverage {int n->
        if (n <= 0) {
            throw new IllegalArgumentException("移動平均算出でn=${n}が指定されました。")
        }
        if (empty) {
            return []
        }
        if (n > size()) {
            n = size()
        }
        (1..<n).collect{delegate[0..<it].average} +
        (n..size()).collect {i->delegate[(i-n)..<i].average}
    }

テストしてみたら通りました。これで異常系も一通り大丈夫そうです。

計算量について

上記のロジックだと、(リストの件数-n)回、n個の値の平均を算出するので、計算量は指定したnの数値とリストの長さのそれぞれに比例します。
nが大きくなる場合は、移動平均 - Wikipediaにあるように、「前回の移動平均値 - l[i-n]/n + l[i]/n」で計算できますが、この場合誤差が積み重なってしまいますので、移動平均値の代わりに合計値を取っておいて「移動合計値 = (前回の移動合計値 - l[i-n] + l[i]); 移動平均値 = 移動合計値 / n」とするのが良さそうです。移動合計値なんて言葉が有るかどうかは置いておいて。

…が、めんどくさいのでここまでにします^^;

防御率計算DSLの強化

http://d.hatena.ne.jp/uokumura/20090427/1240854856で作った防御率計算スクリプトですが、成績の投入が

[
  "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]],
]

DSLと呼ぶにはあまりに不細工な物でした。
こいつをちょっと強化して、

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

と書けるようにしてみました。

まずはテスト

大きく手を入れるので、テストが無いと壊しちゃう可能性大です。さぼってましたが、そろそろテストを書く頃合いのようです。
…と真面目にテストを書き出すと、それだけでへとへとになっちゃうので、ここは手抜きが肝要。とりあえずテストなしで出来そうな、出力フォーマットをちょっと手直しと今日までのデータの投入をしてしまって、まずは結果を取ります。そうすれば、あとはその結果をそのまま使ってassert文を書くだけ。基本的にリファクタリングなので、これで十分なのです。

assert 阪神成績.チーム防御率グラフ ==
         "http://chart.apis.google.com/chart?cht=bvg&chbh=5,0,10&chm=D,0000ff,1,0,2&chco=00ffff,ffffff&chxt=y&chs=620x200&chxr=0,0,10.00&chd=s:MSk9MYibbqSGYbNiAAGNYkMSYANNb,MPWgcbcccdcaaaZaYWWVVWWVVVUUU"

assert 阪神成績.バッテリー別成績 == 
"""|* |*狩野|*岡崎|*清水|*全捕手|
|*能見|40回1/3 自責点12 防御率2.68|-|-|40回1/3 自責点12 防御率2.68|
|*安藤|37回0/3 自責点13 防御率3.16|-|-|37回0/3 自責点13 防御率3.16|
|*下柳|30回1/3 自責点13 防御率3.86|-|-|30回1/3 自責点13 防御率3.86|
|*久保|13回0/3 自責点4 防御率2.77|17回1/3 自責点10 防御率5.19|-|30回1/3 自責点14 防御率4.15|
|*福原|23回1/3 自責点14 防御率5.40|-|-|23回1/3 自責点14 防御率5.40|
|*江草|11回0/3 自責点1 防御率0.82|2回1/3 自責点0 防御率0.00|1回0/3 自責点0 防御率0.00|14回1/3 自責点1 防御率0.63|
|*渡辺|12回1/3 自責点4 防御率2.92|-|1回0/3 自責点0 防御率0.00|13回1/3 自責点4 防御率2.70|
|*アッチソン|10回1/3 自責点7 防御率6.10|3回0/3 自責点0 防御率0.00|-|13回1/3 自責点7 防御率4.72|
|*ジェン|11回1/3 自責点5 防御率3.97|-|-|11回1/3 自責点5 防御率3.97|
|*阿部|6回1/3 自責点0 防御率0.00|2回2/3 自責点2 防御率6.75|1回0/3 自責点0 防御率0.00|10回0/3 自責点2 防御率1.80|
|*ウィリアムス|8回1/3 自責点0 防御率0.00|1回1/3 自責点4 防御率27.00|-|9回2/3 自責点4 防御率3.72|
|*石川|9回0/3 自責点6 防御率6.00|-|-|9回0/3 自責点6 防御率6.00|
|*藤川|7回0/3 自責点1 防御率1.29|1回0/3 自責点1 防御率9.00|-|8回0/3 自責点2 防御率2.25|
|*筒井|5回1/3 自責点0 防御率0.00|-|-|5回1/3 自責点0 防御率0.00|
|*藤田|-|1回1/3 自責点1 防御率6.75|-|1回1/3 自責点1 防御率6.75|
|*金村大|-|-|1回0/3 自責点0 防御率0.00|1回0/3 自責点0 防御率0.00|
|*全投手|225回0/3 自責点80 防御率3.20|29回0/3 自責点18 防御率5.59|4回0/3 自責点0 防御率0.00|258回0/3 自責点98 防御率3.42|
"""

修正内容

計算内容もデータも変わらないので、PichingStatsとCompositePichingStatsはそのままです。

import java.text.SimpleDateFormat

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 自責点${自責点} 防御率${防御率}"
    }
}

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
    }
}

「チーム」クラスは「通算成績」クラスに名前を変えました。
こいつが解析する親玉なので、大幅に手を入れてます。といっても構造は変わらないので、解析周りだけですが。

public class 通算成績 {
    private static final FORMAT = new SimpleDateFormat("y/M/d")

    def 全試合 = new CompositePitchingStats()
    
    private def 投手リスト = []
    private def 捕手リスト = []

    public 通算成績(Closure 全試合DSL) {
        全試合.addAll(this.with(全試合DSL).collect{key, value->
            new 試合(通算成績:this, 日付: FORMAT.parse("2009/$key"), バッテリー登板リスト:value)
        })
    }
    
    private get投手(String 投手名) {
        def 投手 = 投手リスト.find{it.名前 == 投手名}
        if (投手 == null) {
            投手リスト << (投手 = new 選手(名前:投手名))
        }
        return 投手
    }
    
    private get捕手(String 捕手名) {
        def 捕手 = 捕手リスト.find{it.名前 == 捕手名}
        if (捕手 == null) {
            捕手リスト << (捕手 = new 選手(名前:捕手名))
        }
        return 捕手
    }
    
    def methodMissing(String name, args) {
        def match = (name =~ /^(.*)x(.*)$/)
        if (!match) {
            throw new MissingMethodException(name, this, args)
        }
        return new バッテリー登板([投手:get投手(match[0][1]), 捕手:get捕手(match[0][2])] + args[0])
    }
    
    String getバッテリー別成績() {
        "|* |*${捕手リスト*.名前.join('|*')}|*全捕手|\n" + 
        投手リスト.sort{[it.アウト数, it.登板回数]}.reverse().collect{投手->
            "|*${投手.名前}|" + 捕手リスト.collect{捕手->
                new CompositePitchingStats(投手.findAll{it.捕手 == 捕手}).toString()
            }.join("|") + "|${投手.toString()}|"
        }.join("\n") + "\n" +
        "|*全投手|${捕手リスト*.toString().join('|')}|${全試合.toString()}|\n"
    }
    
    String getチーム防御率グラフ() {
        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 graphData = [全試合*.防御率, 全試合*.累積成績*.防御率]
        def max = graphData.flatten().max()
        
        "http://chart.apis.google.com/chart" +
            "?cht=bvg&chbh=5,0,10&chm=D,0000ff,1,0,2&chco=00ffff,ffffff&chxt=y" +
            "&chs=${全試合.size()*20+40}x200&chxr=0,0,${max}" + 
            "&chd=s:${graphData.collect{simpleEncode(it, max)}.join(',')}"
    }
}

toStringは「getバッテリー成績」メソッドに名前を変え、またルーズステートメントで書いていたグラフ表示は「getチーム防御率グラフ」メソッドとして取り込んでます。これは内容はそのままの、ただのメソッドの移動です。
ポイントはコンストラクタとmethodMissing。「投手x捕手(投球回:7.1, 自責点: 2)」は、実はこの「通算成績」のメソッド呼び出しになってるんです。コンストラクタで受け取ったDSLをthis.with(DSL)とこのクラスのメソッドを使って実行してやるんですね。で、メソッドの書式で書かれたものをmethodMissingで捕まえる。methodMissingではメソッド名(投手x捕手)から投手名と捕手名を取り、それと()内のその他のデータを加えて1回のバッテリー登板のコンストラクタに入れてます。
これでDSLを実行するだけで、日付をキーにして「その日の試合で登板した各バッテリーごとの成績のリスト」が入ったMapが出来るわけです。ここまで出来れば、後はもう前回作ったスクリプトの世界です。

class 試合 {
    @Delegate CompositePitchingStats バッテリー登板リスト = new CompositePitchingStats()
    
    Date 日付
    def 通算成績
    
    void setバッテリー登板リスト(List<バッテリー登板> data) {
         バッテリー登板リスト = new CompositePitchingStats(data)
    }
    
    public PitchingStats get累積成績() {
        new CompositePitchingStats(通算成績.全試合.findAll{it.日付 <= 日付})
    }
}

class バッテリー登板 extends PitchingStats {
    def 投手
    def 捕手
    int アウト数
    int 自責点
    
    public void set投手(選手 投手) {
        this.@投手 = 投手
        投手.add(this)
    }
    
    public void set捕手(選手 捕手) {
        this.@捕手 = 捕手
        捕手.add(this)
    }
    
    /** 6回と1/3の投球(19アウト)を6.1のような表記で設定するためのセッター */
    public void set投球回(BigDecimal 投球回) {
        アウト数 = 投球回.intValue() * 3 + (投球回*10 - 投球回.intValue() * 10)
    }
    
    def call(Map args) {
        投球回 = args.投球回
        自責点 = args.自責点
        
        return this
    }
    
    String toString() { "${投手.名前} - ${捕手.名前} ${(int)アウト数 / 3}${アウト数 % 3}/3 自責点${自責点}"}
}

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

バッテリー登板のプロパティのセットはDSLがやってくれるようになったので、もう「バッテリー登板」がStringのマップから自分を構築する必要が無くなり、その結果「通算成績(旧「チーム」)」を知ってる必要も無くなりました。その結果ちょっとコードが減ってますが、ほとんどそのままです。

動かしてみる

「通算成績」クラスのコンストラクタに、DSLを記述したクロージャを渡します。
外だししたファイルの内容をクロージャとして取ってくるようにすれば、完全にDSLになるんですけど、まぁそこまではやりませんでした。

def 阪神成績 = new 通算成績({[
     "4/3": [安藤x狩野(投球回:7, 自責点:2), ウィリアムスx狩野(投球回:1, 自責点:0), 藤川x狩野(投球回:1, 自責点:0)],
     "4/4": [能見x狩野(投球回:5, 自責点:3), 渡辺x狩野(投球回:1, 自責点:0), 阿部x岡崎(投球回:1, 自責点:0), 藤田x岡崎(投球回:1, 自責点:0), 江草x清水(投球回:1, 自責点:0)],
     "4/5": [福原x狩野(投球回:5, 自責点:6), 渡辺x狩野(投球回:1, 自責点:0), 江草x狩野(投球回:1, 自責点:0), アッチソンx狩野(投球回:1, 自責点:0), ウィリアムスx岡崎(投球回:1, 自責点:0)],
     "4/7": [久保x岡崎(投球回:6.1, 自責点:5), ウィリアムスx岡崎(投球回:0.1, 自責点:4), 藤田x岡崎(投球回:0.1, 自責点:1), 阿部x岡崎(投球回:0.2, 自責点:0), 江草x岡崎(投球回:1.1, 自責点:0)],
     "4/8": [下柳x狩野(投球回:6, 自責点:2), 渡辺x狩野(投球回:2, 自責点:0), 阿部x狩野(投球回:1, 自責点:0)],
     "4/9": [石川x狩野(投球回:7, 自責点:4), 江草x岡崎(投球回:1, 自責点:0), アッチソンx岡崎(投球回:1, 自責点:0)],
    "4/10": [安藤x狩野(投球回:5, 自責点:3), 江草x狩野(投球回:1, 自責点:1), 渡辺x狩野(投球回:1, 自責点:1), ウィリアムスx狩野(投球回:1, 自責点:0)],
    "4/11": [能見x狩野(投球回:6.1, 自責点:4), アッチソンx狩野(投球回:0.2, 自責点:0), 阿部x狩野(投球回:1, 自責点:0)],
    "4/12": [福原x狩野(投球回:5, 自責点:3), アッチソンx狩野(投球回:1, 自責点:3), 江草x狩野(投球回:1, 自責点:0), ウィリアムスx狩野(投球回:1, 自責点:0), 藤川x狩野(投球回:2, 自責点:0), 渡辺x狩野(投球回:2, 自責点:0)],
    "4/15": [下柳x狩野(投球回:6.1, 自責点:5), 渡辺x狩野(投球回:0.2, 自責点:0), 石川x狩野(投球回:2, 自責点:2)],
    "4/16": [久保x岡崎(投球回:6, 自責点:2), アッチソンx岡崎(投球回:2, 自責点:0), 藤川x岡崎(投球回:1, 自責点:1)],
    "4/17": [安藤x狩野(投球回:7, 自責点:1), アッチソンx狩野(投球回:0.2, 自責点:0), ウィリアムスx狩野(投球回:0.1, 自責点:0), 江草x狩野(投球回:1, 自責点:0)],
    "4/18": [能見x狩野(投球回:6, 自責点:0), 渡辺x狩野(投球回:0.1, 自責点:2), アッチソンx狩野(投球回:0.2, 自責点:2), ウィリアムスx狩野(投球回:1, 自責点:0), 江草x狩野(投球回:1, 自責点:0)],
    "4/19": [福原x狩野(投球回:5.1, 自責点:4), 渡辺x狩野(投球回:0.2, 自責点:0), 阿部x狩野(投球回:1, 自責点:0), 江草x狩野(投球回:1, 自責点:0)],
    "4/21": [下柳x狩野(投球回:6, 自責点:2), 渡辺x狩野(投球回:1, 自責点:0), 江草x狩野(投球回:1, 自責点:0)],
    "4/22": [久保x岡崎(投球回:5, 自責点:3), 阿部x岡崎(投球回:1, 自責点:2), 阿部x狩野(投球回:1, 自責点:0), 筒井x狩野(投球回:1, 自責点:0)],
    "4/23": [安藤x狩野(投球回:7, 自責点:0), ウィリアムスx狩野(投球回:1, 自責点:0), アッチソンx狩野(投球回:2, 自責点:0), 藤川x狩野(投球回:2, 自責点:0)],
    "4/24": [能見x狩野(投球回:9, 自責点:0)],
    "4/25": [福原x狩野(投球回:8, 自責点:1), 筒井x狩野(投球回:1, 自責点:0)],
    "4/26": [ジェンx狩野(投球回:6, 自責点:0), アッチソンx狩野(投球回:0.1, 自責点:2), 江草x狩野(投球回:1.2, 自責点:0)],
    "4/28": [下柳x狩野(投球回:6, 自責点:4), 渡辺x狩野(投球回:0.2, 自責点:0), 江草x狩野(投球回:0.1, 自責点:0), ウィリアムスx狩野(投球回:1, 自責点:0), 藤川x狩野(投球回:1, 自責点:0)],
    "4/29": [安藤x狩野(投球回:3, 自責点:5), 阿部x狩野(投球回:2, 自責点:0), アッチソンx狩野(投球回:2, 自責点:0), 渡辺x狩野(投球回:1, 自責点:1), 筒井x狩野(投球回:1, 自責点:0)],
    "4/30": [久保x狩野(投球回:6.2, 自責点:2), 江草x狩野(投球回:0.1, 自責点:0), ウィリアムスx狩野(投球回:1, 自責点:0), アッチソンx狩野(投球回:1, 自責点:0)],
    " 5/2": [能見x狩野(投球回:7, 自責点:2), ウィリアムスx狩野(投球回:1, 自責点:0), 藤川x狩野(投球回:1, 自責点:1)],
    " 5/3": [ジェンx狩野(投球回:5, 自責点:4), 筒井x狩野(投球回:1, 自責点:0), 渡辺x清水(投球回:1, 自責点:0), 阿部x清水(投球回:1, 自責点:0), 金村大x清水(投球回:1, 自責点:0)],
    " 5/4": [下柳x狩野(投球回:6, 自責点:0), 江草x狩野(投球回:1, 自責点:0), アッチソンx狩野(投球回:1, 自責点:0), 筒井x狩野(投球回:1, 自責点:0)],
    " 5/7": [久保x狩野(投球回:6.1, 自責点:2), 江草x狩野(投球回:0.2, 自責点:0), 渡辺x狩野(投球回:1, 自責点:0)],
    " 5/8": [安藤x狩野(投球回:8, 自責点:2)],
    " 5/9": [能見x狩野(投球回:7, 自責点:3), ジェンx狩野(投球回:0.1, 自責点:1), 筒井x狩野(投球回:0.1, 自責点:0), 阿部x狩野(投球回:0.1, 自責点:0)],
]})

println 阪神成績.バッテリー別成績
println 阪神成績.チーム防御率グラフ

以下のような結果が取れました。

狩野 岡崎 清水 全捕手
能見 40回1/3 自責点12 防御率2.68 - - 40回1/3 自責点12 防御率2.68
安藤 37回0/3 自責点13 防御率3.16 - - 37回0/3 自責点13 防御率3.16
下柳 30回1/3 自責点13 防御率3.86 - - 30回1/3 自責点13 防御率3.86
久保 13回0/3 自責点4 防御率2.77 17回1/3 自責点10 防御率5.19 - 30回1/3 自責点14 防御率4.15
福原 23回1/3 自責点14 防御率5.40 - - 23回1/3 自責点14 防御率5.40
江草 11回0/3 自責点1 防御率0.82 2回1/3 自責点0 防御率0.00 1回0/3 自責点0 防御率0.00 14回1/3 自責点1 防御率0.63
渡辺 12回1/3 自責点4 防御率2.92 - 1回0/3 自責点0 防御率0.00 13回1/3 自責点4 防御率2.70
アッチソン 10回1/3 自責点7 防御率6.10 3回0/3 自責点0 防御率0.00 - 13回1/3 自責点7 防御率4.72
ジェン 11回1/3 自責点5 防御率3.97 - - 11回1/3 自責点5 防御率3.97
阿部 6回1/3 自責点0 防御率0.00 2回2/3 自責点2 防御率6.75 1回0/3 自責点0 防御率0.00 10回0/3 自責点2 防御率1.80
ウィリアムス 8回1/3 自責点0 防御率0.00 1回1/3 自責点4 防御率27.00 - 9回2/3 自責点4 防御率3.72
石川 9回0/3 自責点6 防御率6.00 - - 9回0/3 自責点6 防御率6.00
藤川 7回0/3 自責点1 防御率1.29 1回0/3 自責点1 防御率9.00 - 8回0/3 自責点2 防御率2.25
筒井 5回1/3 自責点0 防御率0.00 - - 5回1/3 自責点0 防御率0.00
藤田 - 1回1/3 自責点1 防御率6.75 - 1回1/3 自責点1 防御率6.75
金村大 - - 1回0/3 自責点0 防御率0.00 1回0/3 自責点0 防御率0.00
全投手 225回0/3 自責点80 防御率3.20 29回0/3 自責点18 防御率5.59 4回0/3 自責点0 防御率0.00 258回0/3 自責点98 防御率3.42

チーム防御率はそろそろ頭打ちでしょうか。2点台を伺うようになると、貯金も出来てくるんでしょうけどねー。まぁ、それよりも打線が…

割引キャッシュフロー計算

コーポレートファイナンスを勉強中です。…ていっても1月以上かけて300ページちょっとしか読めてないんですけど…
で、割引キャッシュフローを計算する問題がいっぱい出てきます。

0年 1年 2年
設備 -100
販促 -50 -30 -20
費用 -50 -100 -120
売上 150 300 360

みたいな表があって、NPVを計算しなさい、て問題がいっぱい書いてあるんですけど、ちょっと計算が面倒なのではしょってました。
ちょっと電計機に計算させれば済む事をしないなんてソフトウェア技術者の名折れです。このままではいけません。そこで、この表を

[
    [-100],
    [-50, -30, -30],
    [-50,-100,-120],
    [100, 200, 240],
]

みたいに[C0, C1, C2, …,Cn]のようなキャッシュフローのリストのリストで投入してNPVを計算できるようなスクリプトを書いてみることにしました。

/** 現在価値 = 将来のキャッシュフロー / (1 + 割引率)^期間 */
class PresentValue {
    Number futureValue
    Number earningRate = 0.1G
    int years = 0
    
    def getPv(){ futureValue / ((1+earningRate) ** years) }
}

/** NPV(純現在価値) = Σ(各時期でのキャッシュフローの現在価値) */
class NetPresentValue {
    def presentValues = []
    
    public NetPresentValue(Map properties = [:], List<Number> futureValues) {
        int years = 0
        presentValues = futureValues.collect{
            new PresentValue(properties + [futureValue:it, years:years++])
        }
    }
    
    Number getNpv(){ presentValues*.pv.sum() }
}

/* 動作確認 */
// 0年目のみならNPV = CF
assert (new NetPresentValue([40]).npv == 40)
// デフォルト割引率の1割を相殺して将来価値を計算 
assert ((int)new NetPresentValue([-4000, (2100 * 1.1), (2100 * 1.1**2)]).npv == 200) 
// 割引率2割を指定して、同様に年率2割の将来価値と相殺 
assert ((int)new NetPresentValue([-4000, (2100 * 1.2), (2100 * 1.2**2)], earningRate:0.2).npv == 200)

PresentValue#getPvとかNetPresentValue#getNpvとか、かっこわるい事この上ありません。
計算前の情報は将来のキャッシュフローやその集合なので、そう言った名前をつけるべきだったと思います。そもそもOOよりも関数型の方が素直な気もしますが、ちょっとデータ弄るとかがめんどくさくなりそうだったので、小細工しやすいようになれたOOで書きました。
まぁ用は足りるので、よしとします。

これで、

[
    [-100],
    [-50, -30, -30],
    [-50,-100,-120],
    [100, 200, 240],
].collect{new NetPresentValue(it)}*.npv.sum()

とやればデフォルトの割引率1割でNPV計算、

[
    [-100],
    [-50, -30, -30],
    [-50,-100,-120],
    [100, 200, 240],
].collect{new NetPresentValue(it, earningRate:0.2)}*.npv.sum()

とやれば割引率2割でのNPV計算、が出来るようになりました。

うーん、もうちょっと…手を入れたいですねー。

できたよ!

Grails goes on: 惜しいリストの、

def merge = { x, y, r = [] -> x == [] ? r : merge(x-x[0], y-y[0], r+[[x[0] , y[0]]]) }

まぁ記法がいいかどうかは別として, 意味は分かると思う. ところが Groovy1.5 でこれは動かないのだ. merge の定義中に再帰的に現れる merge を理解できないのだ. そこで次のようにすれば動く.

def merge; merge = { x, y, r = [] -> x == [] ? r : merge(x-x[0], y-y[0], r+[[x[0], y[0]]]) }

馬鹿でしょ. ま, いいけど.

て言うのが出来そうなのをちょっと思いつきました。

merge = { x, y, r = [] -> x == [] ? r : merge(x-x[0], y-y[0], r+[[x[0] , y[0]]]) }

assert merge([1,2,3], [4,5,6]) == [[1,4],[2,5],[3,6]]

やっぱりできた!…まぁ、アレですね。
たぶん1.5でも動きそうな気がします。


まぁ、クロージャは「その時点でのコンテキストを引き継ぐ」もので、代入の右辺は左辺よりも先に解決されるから

def merge = { x, y, r = [] -> x == [] ? r : merge(x-x[0], y-y[0], r+[[x[0] , y[0]]]) }

で右辺が解決される時点ではまだmerge変数が定義されてなくて見えないのは仕様的には正しいんでしょうけど…不便ですねー。
宣言 -> 右辺解決 -> 代入、の順で評価してくれれば済むのに…

ややこしいのはthisではない:JS

続いて元記事JavaScriptのthisではまった - No Programming, No Lifeの方も。

JavaScriptのthisではまったのでメモしておきます。

とあるのですが、さきほどのid:katzchang氏も

元記事に対して「どのオブジェクト上で走るかの話で、javaのthisも同じっちゃ同じ。 」ってブクマコメントを残した

と指摘されているようにthisは関係ないように思います。
thisの意味はそんなに変わらなくて、どちらもメソッドの持ち主がthisです。jsの場合メソッド(関数)が代入可能で、このJavaにはない「メソッドを代入する」事がハマったポイントではないかと思いました。

<html>
<head>
<title>thisのテスト</title>
<script type="text/javascript">
var someObj = {
    name   : 'SomeObject',
    whoAmI : function() {
        alert('I am ' + this.name + '.')
    }
}
function init() {
    var button1 = document.getElementById('button1')
    var button2 = document.getElementById('button2')
    button1.onclick = someObj.whoAmI
    button2.onclick = function(){ someObj.whoAmI() }
}
</script>
</head>
<body onLoad="init()">
<button id="button1" name="ぼたん1">押してね1</button><br>
<button id="button2" name="ぼたん2">押してね2</button><br>
</body>
</html>

とやるとボタン1では「I am ぼたん1」と出て、ボタン2では「I am someObject」と出るという話です。

javascriptのオブジェクトがマップであり、メソッドもそのマップに関数を入れている事を思い出して、ちょっとMapらしく書き換えてみます。

<script type="text/javascript">
var someObj = {}
someObj['name'] = 'SomeObject'
someObj['whoAmI'] = function() {
        alert('I am ' + this.name + '.')
}

function init() {
    var button1 = document.getElementById('button1')
    var button2 = document.getElementById('button2')
    button1['onclick'] = someObj['whoAmI']
    button2['onclick'] = function(){ someObj.whoAmI() }
}
</script>

someObj['whoAmI']function() { alert('I am ' + this.name + '.') }なので、button1['onclick']function() { alert('I am ' + this.name + '.') }になります。

<script type="text/javascript">
var someObj = {}
someObj['name'] = 'SomeObject'
someObj['whoAmI'] = function() {
        alert('I am ' + this.name + '.')
}

function init() {
    var button1 = document.getElementById('button1')
    var button2 = document.getElementById('button2')
    button1['onclick'] = function(){ alert('I am ' + this.name + '.') }
    button2['onclick'] = function(){ someObj.whoAmI() }
}
</script>

button1.onclick()を呼び出せば、this.nameは「ぼたん1」となるわけです。

var someObj = {
    name   : 'SomeObject',
    whoAmI : function() { alert('I am ' + this.name + '.') }
}

とあって、

    button1.onclick = someObj.whoAmI

と書いたときに、それが

    button1.onclick = function(){ someObj.whoAmI() }

ではなく

    button1.onclick = function() { alert('I am ' + this.name + '.') }

という意味だ、というのがポイントなんだと思います。

function() {
    alert('I am ' + this.name + '.')
}

という関数オブジェクトは、「その時のオーナー(this)のnameというフィールドを使ってアラートする」という処理が行われるということです。

というのはそうなんですが、その時々のthisの意味に注目するよりも、メソッドメソッドを代入する、という事がどういう事なのか、に注目した方が良いのではないかと思います。
「オーナー(this)」というのはメソッドの持ち主のことで、それはJavaと変わりない。

ある関数の中でthisはオーナー(呼び出し主)を指している。

の「呼び出し主」がセンダー(メソッドの呼び出し元)の意味なら誤りで、「オーナー」に当たるのはJavaのthisと同じ、そのメソッドの持ち主なわけです。

ややこしいのはthisではない

thisはメソッドとフィールドで微妙に違う件 - @katzchang.contextsを見て。Javaのthisがややこしいという話なんですが、ややこしいのはthisではないです。

結論としては、メソッドとフィールドの扱いは違うっぽい。

  • メソッドを参照する場合、thisは実行時のインスタンスを指す。
  • フィールドを参照する場合、thisは定義時のフィールドを指す。

メソッド(正確にはインスタンスメソッド)とフィールドでちがう、というのはあってるんですけど、thisは関係ありません。メソッドとフィールドで扱いがちがうのはthisに限らないんです。ただの変数でも、評価結果でも、インスタンスメソッドだけが特別扱いです。
実際、

検証

  • Hoge#toString()の中で使われるthisは、SubHogeクラスのインスタンス上で実行しても、Hogeクラスインスタンスのnameフィールドを示す。SubHogeクラスインスタンスのnameフィールドではない。
  • subHogeをSubHogeクラスとして扱う場合とHogeでキャストした場合とで、subHoge.nameの示す値が自然に変わってたりする。
public class Hoge {
    public String name = "Hoge";
    public void foo() {
        System.out.println(this.toString());
    }
    @Override
    public String toString() {
        return this.name;
    }
    
    public static class SubHoge extends Hoge {
        public String name = "SubHoge";
    }
    
    public static void main(String...args) {
        //当然のケース。
        Hoge hoge = new Hoge();
        hoge.foo(); //prints "Hoge".
        System.out.println(hoge.name);
        
        //サブクラスのオブジェクトの動作。
        SubHoge subHoge = new SubHoge();
        subHoge.foo(); //prints "Hoge".
        System.out.println(subHoge.name); //prints "SubHoge".
        System.out.println(((Hoge)subHoge).name); //prints "Hoge".
    }
}

と、検証されてるんですが、

System.out.println(subHoge.name); //prints "SubHoge".
System.out.println(((Hoge)subHoge).name); //prints "Hoge".

と、thisは全く関係なくフィールドへのアクセスが静的な型で決定される事が確認できます。

subHoge.foo(); //prints "Hoge".

となるのは、fooメソッドHogeクラスで定義されているためにここでのthisはHoge型の変数(というか暗黙の引数)となるからで、これはフィールドアクセスだろうがメソッドアクセスだろうが一緒です。