風向と風速の計算について – ベクトル平均


 実投入したWeather Meters だが、これで観測される風向および風速については、あくまでもその瞬間のデータとなる。だが、いわゆる「風向」「風速」とは瞬間風向、瞬間風速のことではなく、10分間の平均風向・風速を指すらしい。

 ここで問題がひとつある。風向、風速とは平たく言えば風の単位ベクトルとスカラーのことであり、風速はともかく風向の平均とはベクトル平均のことになる。算術平均を計算してしまうと、例えば 350 度と 10 度の平均は 0 度になるべきなのに 180 度になってしまい、全く正反対の結果になる。角度の平均については「角度の平均、角度の分散について」が詳しいが、気象庁では単位ベクトル平均で計算しているらしい。なので、今回は単位ベクトル平均で計算したい。とりあえずお手軽に Ruby + SQLite3 で書いてみた。

 まず、いわゆる風向は 0 ≦ θ < 360 の度数法で得られるが、単位ベクトルを計算するために三角関数を使う必要があり、Ruby の Math モジュールの引数はラジアンなのでラジアンに変換する必要がある。これは単純計算で

になる。次に、角度を単位ベクトルに変換するわけだが、要は半径 1 の円周上の点になるので

これで各ベクトル成分が求まるので、これを成分ごとに算術平均を計算すればいい。ただ、Weather Meters の風向については単純に 16 方位しか取れない。そのため、風向のデータをその場で単位ベクトル化するのではなく、予め計算したテーブルで変換したほうが負荷が少ない。

UNIT_VECTOR_TABLE = {
  -1.0   => [ nil, nil ],
  0.0   => [ 1.0, 0.0 ],
  22.5  => [ 0.923879532511287, 0.38268343236509 ],
  45.0  => [ 0.707106781186548, 0.707106781186547 ],
  67.5  => [ 0.38268343236509,  0.923879532511287 ],
  90.0  => [ 0, 1.0 ],
  112.5 => [ -0.38268343236509,  0.923879532511287 ],
  135.0 => [ -0.707106781186547, 0.707106781186548 ],
  157.5 => [ -0.923879532511287, 0.38268343236509 ],
  180.0 => [ -1.0, 0 ],
  202.5 => [ -0.923879532511287, -0.38268343236509 ],
  225.0 => [ -0.707106781186548, -0.707106781186547 ],
  247.5 => [ -0.38268343236509,  -0.923879532511287 ],
  270.0 => [ 0, -1.0 ],
  292.5 => [ 0.38268343236509,  -0.923879532511287 ],
  315.0 => [ 0.707106781186547, -0.707106781186548 ],
  337.5 => [ 0.923879532511287, -0.38268343236509 ]
}

後で平均を求める必要があるので、このデータは計測ごとに SQLite などの DB に保存しておく。

 次に単位ベクトル平均を取って角度に戻すわけだが、ベクトル成分を角度に戻すにはタンジェントを取ればいい。タンジェントは X 成分 ÷ Y 成分なので

となり、平均ではなく合計だけ計算すればいいことになる。浮動小数点演算を重ねれば重ねるほど誤差が大きくなるので、あくまで合計を計算するだけにしたほうがいい。

 タンジェントが計算できれば、あとはタンジェントの逆関数で角度に戻し、ラジアンから度数法に戻す。

角度に戻す

 これで角度が求められるが、得られた値については以下の問題がある。

  • タンジェントは -90 度 〜 90 度の 180 度で得られる。
    • 180 度ごとに発散してしまうので、別途置き換える必要がある。
    • X 成分が負のとき = 90度 〜 270 度のときは求められた角度に 180 を足す必要がある。
  • 浮動小数点演算の誤差を考慮する必要がある。

 誤差については最終的に 16 方位に落とせればいいので厳密に考える必要はない。発散については、それを考慮してくれる Math.atan2 があるのだが、値域の関係で使いづらいので Math.atan で計算する。なので、それを踏まえて

# 丸め
x  = (x * 10000).to_i.to_f / 10000.0
y  = (y * 10000).to_i.to_f / 10000.0
direction = 0.0
# 発散する場合またはtan = 0
if x == 1 && y == 0
  direction = 0
elsif x == -1 && y == 0
  direction = 180
elsif x == 0 && y == 1
  direction = 90
elsif x == 0 && y == -1
  direction = 270
else
# arctan から度数に落とす
  direction = ((Math.atan2(y, x) * 180 / Math::PI) + (x < 0 ? 180 : 0)) % 360
end

となるだろうか。ただし、変数 x, y にはそれぞれ X 成分と Y 成分の合計が入っているものとする。

 こんな感じで風向の平均が計算できた。風速については単純にスカラー平均なので、得られた風速のデータを算術平均すればいい。