統計量のクラスの説明¶
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()
のようにして観測統計量オブジェクトを作成します。基本統計量と同じように
value
とcount
が使えます。代入した値の平均は、
i.mean
で取れます。ここでは「10と20」を代入したので、平均は15.0です。
時間統計量クラス¶
StatTime
のオブジェクトも実数のように使えて、代入した値の回数や平均を取得できます。
ただし、平均の計算方法がStatCount
と異なります。
StatTime
のmean
は時間平均を返します。
f(t)
を時刻t
における値とすると、時間平均は下記のように計算します。
時間平均 =
(t0からt1までのf(t)の積分値) /(t1 - t0)
ただし、t0
はStatTime
オブジェクトの初期化の時刻を、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
で管理しています。
_pretime
がStatTime.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を継承してください。これをリスコフの置換原則といいます。 このクエストでは「時間統計量は観測統計量である」としています。 本来は、統計量クラスを新たに作って、観測統計量クラスも時間統計量クラスも統計量クラスから派生した方が良いでしょう。
参考:リスコフの置換原則
この原則が成り立つときでも、継承はなるべく使わない方がよいでしょう。理由は、クラスの結びつきが強くなるためです。その代わりに、結びつきが弱くなる方法としてコンポジション(※)がオススメです。
※ 別のクラスのオブジェクトを部品としてデータ属性に持つ方法をコンポジションといいます。