テストケースを作成する際に、決定表の論理テーブルの部分を書くのが面倒くさかったので、pythonで自動化スクリプトを書いてみました
各因子の水準数のリストをインプットに、下図の赤枠の中の部分をcsvに出力する関数です
以下のような読者の方は参考になるのではないでしょうか?
・決定表を作成するスクリプトが欲しい方
・テスト駆動開発の題材が欲しい方
・pythonでのユニットテストの書き方を知りたい方
環境 ・ ディレクトリ情報
Python
3.9.0
numpy
1.16.6
※unittest
は組み込みで使えます
- root
- main.py
- make_decision_table.py
- test.py
- make_decision_table[2, 3, 2, 2, 2, 2].csv
完成品
-
main.py
からmake_decisioni_table()
を呼んでいます。 - level_list (水準リスト) に自分の欲しい各因子の水準数を追加することで、csv形式の決定表が出力されます。
import numpy as np
from make_decision_table import make_decision_table
def main():
level_list = [2,3,2,2,2,2]
table = make_decision_table(level_list)
np.savetxt(f'make_decision_table{level_list}.csv', table, delimiter=',', fmt='%s')
if __name__ == "__main__":
main()
import numpy as np
def make_decision_table(level_list:list):
def recursive_make_table(n:int):
if n == 1:
res = table_basis(level_list[0])
return res
else:
lower_table = recursive_make_table(n-1)
top = np.repeat(table_basis(level_list[n-1]), lower_table.shape[1], axis=1)
bottom = np.tile(lower_table, level_list[n-1])
res = np.vstack([top, bottom])
return res
n_level = len(level_list)
return recursive_make_table(n_level)
def table_basis(level:int):
res = np.full((level, level), '-')
for i in range(level):
for j in range(level):
if (i == j):
res[i,j] = 'Y'
return res
テスト駆動開発でのプロセス
それではテスト駆動でどのようにこのスクリプトを作り上げていったかを追って説明していきます
まずは、因子:1, 水準数:2の最も単純なテストケースを作成し、それを通るようにコードを書いていきます
import numpy as np
import unittest
from make_decision_table import make_decision_table
class TestMakeDecisionTable(unittest.TestCase):
def test_2(self):
expected = np.array(
[
["Y", "-"],
["-", "Y"]
]
)
level_list = [2]
actual = make_decision_table(level_list)
self.assertEqual(expected, actual)
import numpy as np
def make_decision_table(level_list:list):
return np.array(
[
["Y", "-"],
["-", "Y"]
]
)
テストの期待値と全く同じ配列を返すように実装します。これで1つのテストケースは必ず通るはずですね
decesion_table % python3 -m unittest test
...
if not first == second:
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
おっと、unittest.assertEqual
は numpy.ndarray
に対応していないのでテストに落ちてしまいました。
先に進みたいので、 _assertEqual
を自分で用意しました。
def _assertEqual(self, arrayExpected, arrayActual) -> bool:
for i in range(len(arrayExpected)):
for j in range(len(arrayExpected)):
if (arrayExpected[i,j] != arrayActual[i,j]):
return False
def test_2(self):
expected = np.array(
[
["Y", "-"],
["-", "Y"]
]
)
level_list = [2]
actual = make_decision_table(level_list)
self.assertTrue(self._assertEqual(expected, actual))
print('test_2: ',actual, sep='\n')
% python3 -m unittest test
test_2:
[['Y' '-']
['-' 'Y']]
.
----------------------------------------------------------------------
Ran 1 tests in 0.001s
OK
はい、1つ目のテストケースが通りました
2つ目に移りましょう !!
因子:1, 水準:3のケースを追加します。
import numpy as np
import unittest
from make_decision_table import make_decision_table
class TestMakeDecisionTable(unittest.TestCase):
def test_2(self):
expected = np.array(
[
["Y", "-"],
["-", "Y"]
]
)
level_list = [2]
actual = make_decision_table(level_list)
print('test_2: ',actual, sep='\n')
self.assertTrue(self._assertEqual(expected, actual))
def test_3(self):
expected = np.array(
[
["Y", "-", "-"],
["-", "Y", "-"],
["-", "-", "Y"]
]
)
level_list = [3]
actual = make_decision_table(level_list)
print('test_3: ',actual, sep='\n')
self.assertTrue(self._assertEqual(expected, actual))
def _assertEqual(self, arrayExpected, arrayActual) -> bool:
if (arrayExpected.shape != arrayActual.shape):
print("決定表の配列のサイズが間違っています")
return False
for i in range(len(arrayExpected)):
for j in range(len(arrayExpected)):
if (arrayExpected[i,j] != arrayActual[i,j]):
print(f"決定表の要素[{i},{j}]が異なります")
return False
return True
とりあえず、テストを実行してみましょう。
decesion_table % python3 -m unittest test
test_2:
[['Y' '-']
['-' 'Y']]
.決定表の配列のサイズが間違っています
F
======================================================================
FAIL: test_3 (test.TestMakeDecisionTable)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/nakaitaketo/workspace/decesion_table/test.py", line 30, in test_3
self.assertTrue(self._assertEqual(expected, actual))
AssertionError: False is not true
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
想定通り2個目のテストケースで落ちたので、このケースを通すようにコードを修正しましょう。因子が1の時、水準数がいくつでも正しい決定表が作成できるように修正します。
def make_decision_table(level_list:list):
# return np.array(
# [
# ["Y", "-"],
# ["-", "Y"]
# ]
# )
factor = level_list[0]
res = np.full((factor, factor), '-')
for i in range(factor):
for j in range(factor):
if (i == j):
res[i,j] = 'Y'
return res
decesion_table % python3 -m unittest test
test_2:
[['Y' '-']
['-' 'Y']]
.test_3:
[['Y' '-' '-']
['-' 'Y' '-']
['-' '-' 'Y']]
.
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
はい、通りました! それでは、次のステップでは複数の因子に対応できるようにしたいです!まずは、
因子:2, 水準:[2, 2]のテストケースを追加します。
def test_2by2(self):
expected = np.array(
[
["Y", "Y", "-", "-"],
["-", "-", "Y", "Y"],
["Y", "-", "Y", "-"],
["-", "Y", "-", "Y"]
]
)
level_list = [2, 2]
actual = make_decision_table(level_list)
print('test_2by2: ',actual, sep='\n')
self.assertTrue(self._assertEqual(expected, actual))
import numpy as np
def make_decision_table(level_list:list):
# factor = level_list[0]
# res = np.full((factor, factor), '-')
# for i in range(factor):
# for j in range(factor):
# if (i == j):
# res[i,j] = 'Y'
# return res
if len(level_list) == 1:
res = np.full((level_list[0], level_list[0]), '-')
for i in range(level_list[0]):
for j in range(level_list[0]):
if (i == j):
res[i,j] = 'Y'
return res
elif len(level_list) == 2:
len_of_one_side = sum(level_list)
res = np.full((len_of_one_side, len_of_one_side), '-')
bottom_piece = make_decision_table( [ level_list[0] ] )
bottom = np.hstack([bottom_piece, bottom_piece])
top = top_maker(level1=level_list[0], level2=level_list[1])
res = np.vstack([top, bottom])
return res
else:
return None
def top_maker(level1:int, level2:int):
level1_table = make_decision_table( [ level1 ] )
res = np.repeat(level1_table, level2, axis=1)
return res
少し複雑になってきましたが、因子数(level_list
)が1のときと2のときでif文で処理を分けています。これで、因子=2のときにも対応できるようにします。
テストを実行してみましょう。
nakaitaketo@k3-nakai decesion_table % python3 -m unittest test
test_2:
[['Y' '-']
['-' 'Y']]
.test_2by2:
[['Y' 'Y' '-' '-']
['-' '-' 'Y' 'Y']
['Y' '-' 'Y' '-']
['-' 'Y' '-' 'Y']]
.test_3:
[['Y' '-' '-']
['-' 'Y' '-']
['-' '-' 'Y']]
.
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK
テストが通りました!
さらに汎用化することを前提に、ここまでの結果を元にどうやって決定表を作成できるかを考察してみましょう。
まず、因子:2, 水準: [2,2] のケースを考えます。具体的にはこれを作ることを考えましょう。
[['Y' 'Y' '-' '-']
['-' '-' 'Y' 'Y']
['Y' '-' 'Y' '-']
['-' 'Y' '-' 'Y']]
因子:1, 水準: [2] の決定表はこれでした。ここから作れないでしょうか?
[['Y' '-']
['-' 'Y']]
このように考えました。まず、目標のマトリックスを上下で2つに分けます。
- 上部は、因子:1, 水準: [2] の決定表を列方向に2倍に引き延ばすことで作れます。
[['Y' 'Y' '-' '-']
['-' '-' 'Y' 'Y']
- 下部は、因子:1, 水準: [2] の決定表を列方向に2倍することで作れます。
['Y' '-' 'Y' '-']
['-' 'Y' '-' 'Y']]
これらの処理はpythonのnp.repeat
、np.tile
関数を用いて実現することができます!
numpyさすがです! Ward Cunningham(ウォード・カニンガム) が、
美しいコードは、その言語がまるでその問題を解決するために作られたかのように見せる
と言っていたのを思い出しました。numpyは、この問題を解くために作られたかのようにエレガントですね
考察をコードに反映させましょう。
import numpy as np
def make_decision_table(level_list:list):
if len(level_list) == 1:
res = np.full((level_list[0], level_list[0]), '-')
for i in range(level_list[0]):
for j in range(level_list[0]):
if (i == j):
res[i,j] = 'Y'
return res
elif len(level_list) == 2:
# len_of_one_side = sum(level_list)
# res = np.full((len_of_one_side, len_of_one_side), '-')
# bottom_piece = make_decision_table( [ level_list[0] ] )
# bottom = np.hstack([bottom_piece, bottom_piece])
# top = top_maker(level1=level_list[0], level2=level_list[1])
top = top_maker(level_list[0], level_list[1])
bottom = bottom_maker(level_list[0], level_list[1])
res = np.vstack([top, bottom])
return res
else:
return None
def top_maker(level1:int, level2:int):
top_piece = make_decision_table( [ level1 ] )
res = np.repeat(top_piece, level2, axis=1)
return res
def bottom_maker(level1:int, level2:int):
bottom_piece = make_decision_table( [ level1 ] )
res = np.tile(bottom_piece, level2)
return res
因子数=3のケースに進みましょう!
def test_3by2by2(self):
expected = np.array(
[
["Y", "Y", "Y", "Y", "-", "-", "-", "-", "-", "-", "-", "-"],
["-", "-", "-", "-", "Y", "Y", "Y", "Y", "-", "-", "-", "-"],
["-", "-", "-", "-", "-", "-", "-", "-", "Y", "Y", "Y", "Y"],
["Y", "Y", "-", "-", "Y", "Y", "-", "-", "Y", "Y", "-", "-"],
["-", "-", "Y", "Y", "-", "-", "Y", "Y", "-", "-", "Y", "Y"],
["Y", "-", "Y", "-", "Y", "-", "Y", "-", "Y", "-", "Y", "-"],
["-", "Y", "-", "Y", "-", "Y", "-", "Y", "-", "Y", "-", "Y"]
]
)
level_list = [2, 2, 3]
actual = make_decision_table(level_list)
print('test_3by2by2: ',actual, sep='\n')
self.assertTrue(self._assertEqual(expected, actual))
ここまでくると、因子数によってif文で処理を分けるのは厳しくなってきますね
しかし、先ほどの考察のおかげで、この問題には再帰的な構造があることに気付きました。つまり、より大きな決定表はそれより小さい決定表を使って作成することができるということです。従って、再帰のアルゴリズムを意識しながら、コードを書き換えてみました。
import numpy as np
def make_decision_table(level_list:list):
def recursive_make_table(n:int):
if n == 1:
res = table_basis(level_list[0])
return res
# elif len(level_list) == 2:
# bottom = bottom_maker(level_list[0], level_list[1])
# top = top_maker(level_list[0], level_list[1])
# res = np.vstack([top, bottom])
# return res
# else:
# return None
else:
top = np.repeat(table_basis(level_list[n-1]), len(recursive_make_table(n-1)), axis=1)
bottom = np.tile(recursive_make_table(n-1), level_list[n-1])
res = np.vstack([top, bottom])
return res
n_level = len(level_list)
return recursive_make_table(n_level)
def table_basis(level:int):
res = np.full((level, level), '-')
for i in range(level):
for j in range(level):
if (i == j):
res[i,j] = 'Y'
return res
急に出てきたtable_basis
関数は、この再帰的なロジックの基礎的な部分を構築するのを手助けしてくれます。 再帰関数はマトリョーシカのように、自分より小さい自分自身と相似な構造を次々に呼んでいきますが、必ず、最も小さい部分が出てきます。それは基底 (basis) と呼ばれるものであり、今回の場合では、水準数nの1つ目の因子で、n x n の対角に'Y'がならんだ行列が基底です。加えて、考察のところで見たように、n > 2の場合でも、上部の部分の構造を作るのに必要な関数になってきます。
decesion_table % python3 -m unittest test
test_2:
[['Y' '-']
['-' 'Y']]
.test_2by2:
[['Y' 'Y' '-' '-']
['-' '-' 'Y' 'Y']
['Y' '-' 'Y' '-']
['-' 'Y' '-' 'Y']]
.test_3by2by2:
[['Y' 'Y' 'Y' 'Y' '-' '-' '-' '-' '-' '-' '-' '-']
['-' '-' '-' '-' 'Y' 'Y' 'Y' 'Y' '-' '-' '-' '-']
['-' '-' '-' '-' '-' '-' '-' '-' 'Y' 'Y' 'Y' 'Y']
['Y' 'Y' '-' '-' 'Y' 'Y' '-' '-' 'Y' 'Y' '-' '-']
['-' '-' 'Y' 'Y' '-' '-' 'Y' 'Y' '-' '-' 'Y' 'Y']
['Y' '-' 'Y' '-' 'Y' '-' 'Y' '-' 'Y' '-' 'Y' '-']
['-' 'Y' '-' 'Y' '-' 'Y' '-' 'Y' '-' 'Y' '-' 'Y']]
.test_3:
[['Y' '-' '-']
['-' 'Y' '-']
['-' '-' 'Y']]
.
----------------------------------------------------------------------
Ran 4 tests in 0.003s
OK
はい、通りました! 完成です!
整理したものを以下にあげます。
import numpy as np
def make_decision_table(level_list:list):
def recursive_make_table(n:int):
if n == 1:
res = table_basis(level_list[0])
return res
else:
lower_table = recursive_make_table(n-1)
top = np.repeat(table_basis(level_list[n-1]), lower_table.shape[1], axis=1)
bottom = np.tile(lower_table, level_list[n-1])
res = np.vstack([top, bottom])
return res
n_level = len(level_list)
return recursive_make_table(n_level)
def table_basis(level:int):
res = np.full((level, level), '-')
for i in range(level):
for j in range(level):
if (i == j):
res[i,j] = 'Y'
return res
import numpy as np
from make_decision_table import make_decision_table
def main():
level_list = [2,3,2,2,2,2]
table = make_decision_table(level_list)
np.savetxt(f'make_decision_table{level_list}.csv', table, delimiter=',', fmt='%s')
if __name__ == "__main__":
main()
まとめ
テスト先行で実装することでインクリメンタルに開発を進めることができます。数学の問題を解く時も、簡単なケースから初めて具体的な例を書き出すことを皆さんやられたのではないかなと思います。その感覚と同じで、一見複雑に見える再帰プログラムでも、問題を分割して少しずつ解いていけば解くことができます。一回でポンと正解を導くのではなく、少しずつ問題を理解しながら解いていくのは、問題解決においてとても有効なプロセスではないでしょうか?今回の投稿では、それを実感できたこと、numpyの素晴らしさを再確認できたことがよかったと思います。皆さんも良いテスト駆動LIFEを!
最後までご覧いただき、ありがとうございました!