Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

平均値だけに頼るな:統計の平均とパーセンタイル入門

パフォーマンス監視は、アプリケーションを成功させるために欠かせない重要な取り組みです。何かのパフォーマンスを把握する最も基本的な方法のひとつが、処理が発生するたびにその所要時間を計測し、そこから統計値を導き出すことです。

平均値(Mean)

値の集合の平均値は、対象がどれほど良い、あるいは悪い挙動を示しているかを見るうえでの良い出発点になります。計算方法はシンプルで、対象となるすべての値を合計し、発生回数で割るだけです。

Rubyでレスポンスタイムの平均を計算すると、次のようになります。

def mean(array)
 (array.sum.to_f / array.length).round(2)
end
 
durations = [1,2,3,4,5,6,7,8,9,0]
mean(durations) #=> 4.5

補足:この例では、割り算の精度を保つために合計時間をFloatにキャストしています。これを行わないと、Rubyは小数点以下を切り捨てて最も近い整数を返すため、4.5ではなく4が返されてしまいます。

中央値(Median)

もうひとつ有用な統計値が中央値です。名前は似ていますが、値の集合における平均値と中央値には違いがあります。

中央値とは、データセットを上半分と下半分に分ける境界となる値のことです。奇数個のデータセットの場合は、まず値をソートしてから中央の数を選べば中央値になります。偶数個の場合は、ソートした後、中央の2つの数の平均が中央値となります。

def median(array)
  sorted_array = array.sort
  length = sorted_array.length
 
  if length.odd? # 奇数個の配列なら中央の数
    sorted_array[length / 2]
  else # 偶数個なら中央2つの平均
    first_value = sorted_array[length / 2]
    second_value = sorted_array[length / 2 - 1]
    (first_value + second_value) / 2.to_f
  end
end
 
# 偶数個の配列
durations = [1,2,3,4,5,6,7,8,9,0]
median(durations) #=> 4.5
 
# 奇数個の配列
durations = [1,1,2,3,4,5,6,7,8,9,0]
median(durations) #=> 4

この統計値は、データに大きな偏りやロングテール(長い裾)があるかどうかを確認するのに役立ちます。

durations = [1,2,3,4,5,2000]
 
median(durations) #=> 3.5
mean(durations) #=> 335.83

上記の所要時間の平均値は、2000msというひとつの外れ値の影響を受けて335.83になってしまいます。一方、中央値はわずか3.5であり、データに偏りがあることを示唆しています。

データセットの平均値と中央値を両方計算すれば、大きな外れ値やロングテールの存在を把握できます。

平均値の問題点

平均値と中央値はパフォーマンスの優れた指標ですが、すべてを語ってくれるわけではありません。あるWebページに10回リクエストした場合、平均値は非常に低くても、1回以上のリクエストが完了まで非常に長い時間を要することはあり得ます。

下の画像は、AppSignalのあるアクションについて、99パーセンタイル(青)と90パーセンタイル(緑)、そして平均値(赤)を示したものです。99パーセンタイルと90パーセンタイルが平均値からかなり離れており、スパイクも見られます。つまり、平均的なユーザーは快適な体験をしていても、ときにはページのレンダリングにほぼ2倍の時間待たされるユーザーが存在するということです。理想的には、これらの値をできるだけ互いに近づけ、すべてのユーザーにより一貫した体験を提供したいところです。

例として、10人の顧客がページをリクエストし、所要時間が100ミリ秒から1秒の間である次のようなデータセットを考えてみましょう。

[100,100,100,100,100,100,100,100,100,1_000]

この場合の平均値はわずか190msですが、実際には1人のユーザーが1秒という非常に悪いレスポンスタイムを経験しています。平均値だけを追いかけていると、Webサイトのパフォーマンスは優れていると思い込みやすくなりますが、現実には時折、ひどい体験をするユーザーがいるのです。

上の例は10リクエストの話ですが、もし1日あたり1,000リクエストがあるとしたらどうでしょう。それはつまり、100人のユーザーがひどい経験をしていることを意味します。

パーセンタイル

値の分布をより正確に把握するために使われるのがパーセンタイルです。パーセンタイルは中央値に似た概念で、中央値は「データセットの半分がその数値より下、残りの半分が上」という位置を示します。同様に、20パーセンタイルとは、データセット内の数値の20%がその数値より下にあることを意味します。

次の(ソート済みの)データセットを見てみましょう。

[100,100,200,200,300,300,400,400,500,5_000]

20パーセンタイルを求めたい場合、次のように計算できます。データセットには10個の値があり、求める値は位置1(20.0 / 100 * 10 - 1)にあります(配列はゼロ始まりのため)。この配列は偶数個の要素を含むため、インデックス2とその隣のインデックス3の間の平均を計算する必要があります。結果として、20パーセンタイルの値は150になります。

ごく単純なRuby実装は次のようになります。

def percentile(array, wanted_percentile)
  sorted_array = array.sort
 
  index = (wanted_percentile.to_f / 100) * sorted_array.length - 1
 
  # インデックスが整数でないかチェック
  if index != index.to_i
    sorted_array.at(index.ceil)
  elsif sorted_array.length.even?
    first_value = sorted_array.at(index)
    second_value = sorted_array.at(index + 1)
    (first_value + second_value) / 2
  else
    sorted_array.at(index)
  end
end
 
# 奇数個の数値を含む配列
durations = [100,200,200,300,300,400,400,500,5_000]
 
percentile(durations, 20) #=> 100
percentile(durations, 90) #=> 500
percentile(durations, 95) #=> 5000, インデックスは9.5の小数、切り上げると10
 
# 偶数個の数値を含む配列
durations = [100,100,200,200,300,300,400,400,500,5_000]
 
percentile(durations, 20) #=> 150, インデックス1と2の平均 `(100 + 200) / 2`
percentile(durations, 90) #=> 2750, インデックス8と9の平均 `(500 + 5000) / 2`
percentile(durations, 95) #=> 500, インデックスは8.55の小数、切り上げると9

このpercentile関数は先ほどのmedianの計算とよく似ています。実際、medianは50パーセンタイルと同じものです。

durations = [1,2,3]
 
percentile(durations, 50) == median(durations) #=> true

AppSignalはこれらの統計値を使ってアプリケーションのパフォーマンスメトリクスを生成しています。平均値だけに頼るのではなく、90パーセンタイルと95パーセンタイルも計算することで、外れ値を可視化し、リクエストの分布をより正確に把握できるようにしています。詳しくはパフォーマンステアーページをご覧ください。

注意すべき特殊なケース

パーセンタイルと平均値の計算方法の性質上、90パーセンタイルが平均値を下回ることがあります。たとえば、次のようなデータセットを考えてみましょう。

durations = [1,1,1,1,1,1,1,1,1,1,2000]
 
percentile(durations, 90) #=> 1
mean(durations) #=> 182.73

この場合、平均値は182.73になる一方、90パーセンタイルはわずか1です。

メトリクス収集システムが90パーセンタイルと平均値のみを表示していても、90パーセンタイルが平均値を下回っていれば、データセットのどこかに巨大な外れ値が存在すると推測できます。

👋 この記事を気に入っていただけたら、私たちが書いたRuby(on Rails)パフォーマンスに関する他の記事もぜひご覧ください。Rubyパフォーマンス監視チェックリストも公開しています。

この記事もまもなく100%——まとめ

今回はここまでです!別の記事では、Quantiles(分位点)を使って、すべての顧客のリクエストに対するパーセンタイルを効率的に保存・計算する方法についてお話しする予定です。統計やAPM、エラートラッキング、パフォーマンス監視について質問やご意見があれば、Twitter @AppSignalまたはメールでお気軽にご連絡ください。

  1. Pythonで二分探索木(BST)の中央値をO(n)時間・O(1)空間で求める方法

    問題の概要 二分探索木(Binary Search Tree、BST)が与えられたとき、その中央値を求めることを考えます。ノードの総数を n とすると、中央値は次のように定義されます。 n が奇数の場合: 中央値 = 中序順(昇順)で (n+1)/2 番目のノードの値 n が偶数の場合: 中央値 = (n/2 番目のノードの値 + (n+1)/2 番目のノードの値) / 2 例として、次のようなBSTを考えてみましょう。 7 / \ 4 9 / \ / \ 2 5 8 10 この木の中序走査(昇順)の結

  2. PCが「うなり声」を上げる理由とは?コイル鳴きの原因と効果的な対処法

    最新ゲームのレンダリングや動画編集など、PCに高負荷をかけることが多い方なら、負荷がかかったときにパソコンの音が変化することをご存じでしょう。ヒートシンクの音だけでも、CPUがどれほど頑張っているかをある程度判断できることもあります。しかし、多くのユーザーが予想しない音のひとつが、微かな「うなり声(ウィーンという音)」です。すべてのPCで発生するわけではありませんが、一度聞いてしまえば間違いなく気になってしまうもの。多くのユーザーの報告によると、このうなりはPCが限界近くまで追い込まれたときに発生し、どのアプリを使うと鳴くのかを予測できる人もいるそうです。なぜこのような現象が起きるのでしょうか