統計量のクラスの説明

PyQのクエスト「観測統計量を使った待ち行列」のクラスの説明をします。 また、これらのクラスは、クエスト「ポリモーフィズム(多態性)」のクラスを、より実践的な設計に修正したものになっています。

統計量クラス概要

3つの統計量のクラスと1つのクラスの概要を説明します。

  • StatBase:基本統計量クラス

  • StatCount:観測統計量クラス(StatBaseの派生クラス)

  • StatTime:時間統計量クラス(StatBaseの派生クラス)

3つの統計量クラスのオブジェクトは、実数のように使えます。ただし、どんな値を取ったかの統計量を取ることができます

以下のメソッドやプロパティがあります。

  • reset():統計量の初期化

  • value:値。

  • count:代入した回数。

  • mean:これまでの値の平均値。StatBaseでは使えません。


基本統計量クラス

実数のように使えて、代入した値の回数を参照できます。

class StatBase:
    """基本統計量

    実数のように使えて、代入回数を参照できる。
    """
    def __init__(self):
        self.reset()
        self._value = 0  # 現在値

    def reset(self):
        self._count = 0  # 代入回数

    @property
    def value(self):
        """現在値"""
        return self._value
 
    @value.setter
    def value(self, value):
        self._value = value
        self._count += 1

    @property
    def count(self):
        """代入回数"""
        return self._count

基本統計量の使用例

i = StatBase()
i.value = 10
i.value = 20
print(i.value)  # 20
print(i.count)  # 2
  • i = StatBase() のようにして観測統計量オブジェクトを作成します。

  • 代入は、i.value = 10のようにします。

  • 現在の値の取得は、i.valueで取れます。

  • 代入した回数は、i.countで取れます。


観測統計量クラス

実数のように使えて、代入した値の回数や代入あたりの平均を参照できます。

class StatCount(StatBase):
    """観測統計量

    基本統計量に加えて、代入あたりの平均を参照できる。
    """
    def reset(self):
        super().reset()  # StatBaseの初期化処理
        self._sum = 0  # 代入回数

    @StatBase.value.setter
    def value(self, value):
        self._value = value
        self._count += 1
        self._sum += value

    @property
    def mean(self):
        """代入あたりの平均"""
        return self._sum / max(1, self._count)

観測統計量の使用例

i = StatCount()
i.value = 10
i.value = 20
print(i.value)  # 20
print(i.count)  # 2
print(i.mean)  # 15.0
  • i = StatCount() のようにして観測統計量オブジェクトを作成します。

  • 基本統計量と同じようにvaluecountが使えます。

  • 代入した値の平均は、i.meanで取れます。ここでは「10と20」を代入したので、平均は15.0です。


時間統計量クラス

StatTimeのオブジェクトも実数のように使えて、代入した値の回数や平均を取得できます。 ただし、平均の計算方法がStatCountと異なります。

StatTimemean時間平均を返します。 f(t)を時刻tにおける値とすると、時間平均は下記のように計算します。

時間平均 = (t0からt1までのf(t)の積分値) /(t1 - t0)

ただし、t0StatTimeオブジェクトの初期化の時刻を、t1は現在時刻を表します。

たとえば、人気ラーメン店の店先に並ぶ行列の長さを時間統計量オブジェクトで表したとすると、その平均は行列の時間平均になります。

class StatTime(StatBase):
    """時間統計量

    基本統計量に加えて、単位時間あたりの平均(時間平均)を参照できる。
    """
    env = None  # シミュレーション環境

    def reset(self):
        super().reset()  # StatBaseの初期化処理
        self._sum = 0  # 合計
        self._initime = StatTime.env.now  # 作成時の時刻
        self._pretime = self._initime  # 前回代入時の時刻

    @StatBase.value.setter
    def value(self, value):
        t = StatTime.env.now - self._pretime  # 前回からの時間
        self._pretime = StatTime.env.now  # 時刻を更新
        self._sum += self._value * t
        self._value = value
        self._count += 1

    @property
    def mean(self):
        """時間で見た平均"""
        if self._pretime != StatTime.env.now:
            self.value = self.value
            self._count -= 1  # 上記の代入回数を戻す
        if StatTime.env.now == self._initime:
            return self._value
        else:
            return self._sum / (self._pretime - self._initime)

時間統計量の使用例

ここでは、simpyの知識が必要になります。 simpyの簡単な使い方は、「シミュレーションと待ち行列」で学習できます。

import simpy

# シミュレーション環境作成
StatTime.env = simpy.Environment()
t = StatTime()  # 時間統計量作成

def init_statime():
    """時間統計量の初期化"""
    yield StatTime.env.timeout(9)  # 9時まで待つ
    t.reset()  # 統計量の初期化
    t.value = 1

StatTime.env.process(init_statime())  # init_statimeの登録

def change_statime():
    """時間統計量の更新"""
    yield StatTime.env.timeout(11)  # 11時まで待つ
    t.value = 4

StatTime.env.process(change_statime())  # change_statimeの登録
StatTime.env.run(12)  # シミュレーションを開始し、12時に終了

print(t.mean)  # 2.0
  • StatTimeクラスを使うときは、最初にStatTime.env = simpy.Environment()でシミュレーション環境を作成する必要があります。

  • シミュレーションの時間の単位は利用者が自由に決められます。ここでは時間の単位をhourとします。

  • t = StatTime()で時間統計量オブジェクトを作成します。

  • init_statimeの処理

    • yield StatTime.env.timeout(9)で時刻を9時に進めます。

    • t.reset()で、統計量を初期化します。これにより、平均を計算するときは、9時以降からになります。

    • t.value = 1で、9時での値を1にします。

  • StatTime.env.process(init_statime())で、init_statimeのジェネレーターオブジェクトを登録します。

  • change_statimeの処理

    • yield StatTime.env.timeout(11)で時刻を11時に進めます。

    • t.value = 4で11時での値を4にします。

  • StatTime.env.process(change_statime())で、change_statimeのジェネレーターオブジェクトを登録します。

  • StatTime.env.run(12)でシミュレーションを開始し、12時になるまで、登録された処理を進めます。

  • print(t.mean())時間平均を2と出力します。

12時での時間平均の計算を図で確認してみましょう。

  • 図の青い部分の面積が(1 * (11 - 9) + 4 * (12 - 11)) = 6です。

  • 時間平均は、9時から12時までの3時間で計算します。

  • 時間平均は、6 / 3を計算して2になります。

  • より詳細な計算は後述の「時間平均の計算の流れ」を確認ください。

時間統計量クラスのデータ属性

時間平均は「valueを時間で積分した値」を時間で割ったものです。 「valueを時間で積分した値」は、valueが変化するごとに分解して足し合わせればできます。 そのために、下記のデータ属性を使っています。

  • _initime:時間平均の計算の開始時刻(今回は0)。

  • _pretime:直前にvalueを変更した時刻。

  • _sum_initimeから_pretimeまでの「valueを時間で積分した値」

meanメソッドでやっていること

現在時刻における時間平均(グラフの面積/時間)を計算します。 現在時刻は、StatTime.env.nowで管理しています。 _pretimeStatTime.env.nowになるように、valueメソッドで面積を更新します。 この結果、時間平均は、self._sum / (self._pretime - self._initime)になります。

valueメソッドでやっていること

前回の更新時刻(_pretime)から現在時刻までの面積(self._value * (StatTime.env.now - self._pretime))を_sumに加算し、_pretime, _value, _countを更新します。

時間平均の計算の流れ

  • 9:00:self._sumを0で、self._initimeを9で初期化

  • 9:00:self._valueを1に、self._pretimeを9に設定

  • 11:00:self._sumに「self._value×(現在時刻−前回代入時の時刻)= 1×2」を追加。self._pretimeを11に設定。

  • 12:00:self._sumに「self._value×(現在時刻−前回代入時の時刻)= 4×1」を追加し、時間平均(= (1 * 2 + 4 * 1) / 3 = 2)と計算


継承について

「BはAである」ときだけ、クラスAからクラスBを継承してください。これをリスコフの置換原則といいます。 このクエストでは「時間統計量は観測統計量である」としています。 本来は、統計量クラスを新たに作って、観測統計量クラスも時間統計量クラスも統計量クラスから派生した方が良いでしょう。

参考:リスコフの置換原則

この原則が成り立つときでも、継承はなるべく使わない方がよいでしょう。理由は、クラスの結びつきが強くなるためです。その代わりに、結びつきが弱くなる方法としてコンポジション(※)がオススメです。

※ 別のクラスのオブジェクトを部品としてデータ属性に持つ方法をコンポジションといいます。

当コンテンツの知的財産権は株式会社ビープラウドに所属します。詳しくは利用規約をご確認ください。