Pythonで〇×ゲームのAIを一から作成する　その148　Mbtree_Anim でのフレーム間の差分データの表示

# 目次と前回の記事

https://qiita.com/ysgeso/items/2381dd4e3283cbed49a0

https://qiita.com/ysgeso/items/09a2909f6b44561bd2bf

## これまでに作成したモジュール

以下のリンクから、これまでに作成したモジュールを見ることができます。

| リンク | 説明 |
|:--|:--|
| [marubatsu.py](https://github.com/ysgeso/marubatsu/blob/master/148/marubatsu.py)  | Marubatsu、Marubatsu_GUI クラスの定義|
| [ai.py](https://github.com/ysgeso/marubatsu/blob/master/148/ai.py)  |  AI に関する関数 |
| [test.py](https://github.com/ysgeso/marubatsu/blob/master/148/test.py)  |  テストに関する関数 |
|  [util.py](https://github.com/ysgeso/marubatsu/blob/master/148/util.py) |  ユーティリティ関数の定義。現在は `gui_play` のみ定義されている |
| [tree.py](https://github.com/ysgeso/marubatsu/blob/master/148/tree.py)  | ゲーム木に関する Node、Mbtree クラスの定義  |
| [gui.py](https://github.com/ysgeso/marubatsu/blob/master/148/gui.py)  |  GUI に関する処理を行う基底クラスとなる GUI クラスの定義  |

AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。

https://qiita.com/ysgeso/items/10d1d01192c014173e4b

# Mbtree_Anim のバグの修正と改良

前回の記事では αβ 法での枝狩りが行われたノードの数などの視覚化を行うために、Mbtree_Anim で表示されているアニメーションのフレームまでの、下記の A、P、M、R の値を表示するという改良を行いました。

| 記号 | 意味 | 見出し | 
|:-:|:--|:--|
| __A__  | αβ 法で計算を行ったノードの数  | 計算済 |
| __P__  | αβ 法で枝狩りを行ったノードの数  | 枝狩り |
| __M__  | ミニマックス法で計算を行ったノードの数。M = A + P で計算できる  | 合計 |
| __R__  | M に対する A の比率。A / M で計算できる  | 割合 |

しかし、現状では __枝狩りが行われた際__ に、__いくつのノードの枝狩りが行われたか__ が __わかりづらい__ という問題があります。言葉の説明ではわかりづらいので具体例を挙げます。

下図は下記のプログラムを実行し、4485 フレーム目を表示した際の図で、図の「β 狩り」の文字が赤く表示されていることから、__次のフレームで β 狩りが行われる__ ことがわかります。

```python
from tree import Mbtree, Mbtree_Anim

mbtree = Mbtree(algo="df")
mbtree.calc_score_by_ab(mbtree.root)
Mbtree_Anim(mbtree, isscore=True)
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/f1d2c87e-66d1-3059-fa95-e97994bf9432.png" width="450">

実際に、次の 4486 フレーム目では下図のように枝狩りが行われたノードがさらに暗く表示されます。その際に、__枝狩りが行われたにも関わらず__「枝狩り」の __右の数字が 9179 のまま変化しない__ という問題が発生していることがわかります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/354c96c1-0d6f-8781-bf1a-3f596888739e.png" width="450">

ただし、__その次__ の評価値が確定した 4487 フレーム目では下図のように __「枝狩り」の右の数字__ が 9179 から 14694 に __大きく増え__、赤枠のノードの __評価値が確定__ したので __「計算済」の右の数字が 1 増えています__。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/ba83377e-2eef-64d8-5ee6-bdf049b59432.png" width="450">

上記から、以下の 2 種類の問題がある事がわかります。

* __枝狩りが行われた際__ に、__「枝狩り」の右の数値が増えるフレーム__ が __1 つ遅れる__
* __枝狩り__ によって処理が省略された __ノードの数__ は 14694 - 9179 = 5515 という __計算を行うことで知ることができる__ が、__そのような計算を行うのは面倒__ である

前者についてはバグなので修正し、後者については枝狩りが行われた際に __処理が省略されたノードの数を表示__ するという改良を行うことにします。

# バグの修正

最初に、枝狩りの数値の表示が 1 フレーム遅れて増えるバグを修正します。このバグの原因について少し考えてみて下さい。
## バグの原因の検証と修正

下記は、__`calc_score_by_ab`__ の中で、max ノードの __子ノードの評価値が計算された次のアニメーションのフレームの処理を記録__ するプログラムで、以下のような処理を行っています。この処理にバグの原因があるので、何が原因であるかについて少し考えてみて下さい。

* __1、2 行目__：このフレームで行われた処理に関するデータを `nodelist_by_score` 属性と `ablist_by_score` 属性の要素に追加する
* __4 ~ 8 行目__：β 狩りが行われた場合に残りの子ノードに対して __[`assign_pruned_index`](https://qiita.com/ysgeso/items/289c8067774951433941#pruned_index-%E5%B1%9E%E6%80%A7%E3%82%92%E8%A8%88%E7%AE%97%E3%81%99%E3%82%8B%E9%96%A2%E6%95%B0%E3%81%AE%E5%AE%9A%E7%BE%A9)__ を呼び出すことで、枝狩りが行われたノードに対する処理を行う。__枝狩りが行われたノードの数__ を表す __`num_pruned` 属性の値を増やす処理__ は、__`assign_pruned_index`__ で行われる

```python
1  self.nodelist_by_score.append(node)
2  self.ablist_by_score.append((alpha, beta, None, "update",
3                               self.num_calculated, self.num_pruned))
4  if score >= beta:
5      index = node.children.index(childnode)
6      for prunednode in node.children[index + 1:]:
7          assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
8      break
```

問題の原因は以下の通りです。

* __β 狩りが行われた場合__ は __4 ~ 8 行目__ の処理によって枝狩りが行われた数を計算して __`num_pruned` 属性の値が更新__ される
* __このフレームに関するデータ__ は、__その前の 2 行目で `ablist_by_score` に追加される__ ので、追加されたデータの中の __`self.num_pruned`__ は、__β 狩りが行われる前__ の枝狩りが行われたノードの __数を表している__

従って、このバグを修正するためには __`ablist_by_score` にフレームのデータを追加する処理を__、下記のプログラムのように __β 狩りに関する処理の後で行う__ 必要があります。

* __9、10 行目__：4 行目の後に記述していた `ablist_by_score` にフレームのデータを追加する処理を、β 狩りに関する処理を行った後に移動する
* __5 ~ 8、11、12 行目__：β 狩りに関する処理を行った後で上記の 9 行目の処理が行われるようにするために、8 行目の後にあった break 文を削除し、β 狩りに関する処理を行った後の 11、12 行目にその処理を移動する
* __13 ~ 21 行目__：min ノードに対する処理でも、上記と同様の修正を行う

```python
 1  from marubatsu import Marubatsu
 2
 3  def calc_score_by_ab(self, abroot, debug=False):    
元と同じなので省略
 4                      self.nodelist_by_score.append(node)
 5                      if score >= beta:
 6                          index = node.children.index(childnode)
 7                          for prunednode in node.children[index + 1:]:
 8                              assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
 9                      self.ablist_by_score.append((alpha, beta, None, "update",
10                                                   self.num_calculated, self.num_pruned))
11                      if score >= beta:
12                          break
元と同じなので省略
13                      self.nodelist_by_score.append(node)
14                      if score <= alpha:
15                          index = node.children.index(childnode)
16                          for prunednode in node.children[index + 1:]:
17                              assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
18                      self.ablist_by_score.append((alpha, beta, None, "update",
19                                                   self.num_calculated, self.num_pruned))
20                      if score <= alpha:
21                          break
元と同じなので省略
22      
23  Mbtree.calc_score_by_ab = calc_score_by_ab
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
from marubatsu import Marubatsu

def calc_score_by_ab(self, abroot, debug=False):           
    def assign_pruned_index(node, index):
        node.pruned_index = index
        self.num_pruned += 1
        for childnode in node.children:
            assign_pruned_index(childnode, index)
        
    def calc_ab_score(node, alpha=float("-inf"), beta=float("inf")):
        self.nodelist_by_score.append(node)
        self.ablist_by_score.append((alpha, beta, None, "start",
                                     self.num_calculated, self.num_pruned))
        if node.mb.status != Marubatsu.PLAYING:
            self.calc_score_of_node(node)
            if node.mb.turn == Marubatsu.CIRCLE:
                alpha = node.score
            else:
                beta = node.score
        else:
            if node.mb.turn == Marubatsu.CIRCLE:
                for childnode in node.children:
                    score = calc_ab_score(childnode, alpha, beta)
                    self.nodelist_by_score.append(node)
                    self.ablist_by_score.append((alpha, beta, score, "score",
                                                 self.num_calculated, self.num_pruned))
                    if score > alpha:
                        alpha = score
                    self.nodelist_by_score.append(node)
                    if score >= beta:
                        index = node.children.index(childnode)
                        for prunednode in node.children[index + 1:]:
                            assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
                    self.ablist_by_score.append((alpha, beta, None, "update",
                                                 self.num_calculated, self.num_pruned))
                    if score >= beta:
                        break
                node.score = alpha
            else:
                for childnode in node.children:
                    score = calc_ab_score(childnode, alpha, beta)
                    self.nodelist_by_score.append(node)
                    self.ablist_by_score.append((alpha, beta, score, "score",
                                                 self.num_calculated, self.num_pruned))
                    if score < beta:
                        beta = score
                    self.nodelist_by_score.append(node)
                    if score <= alpha:
                        index = node.children.index(childnode)
                        for prunednode in node.children[index + 1:]:
                            assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
                    self.ablist_by_score.append((alpha, beta, None, "update",
                                                 self.num_calculated, self.num_pruned))
                    if score <= alpha:
                        break
                node.score = beta

        self.nodelist_by_score.append(node)
        self.num_calculated += 1     
        self.ablist_by_score.append((alpha, beta, None, "end",
                                     self.num_calculated, self.num_pruned))
        node.score_index = len(self.nodelist_by_score) - 1     
        return node.score

    from ai import dprint       
    for node in self.nodelist:
        node.score_index = float("inf")
        node.pruned_index = float("inf")
    self.nodelist_by_score = []
    self.ablist_by_score = []
    self.num_calculated = 0
    self.num_pruned = 0
    calc_ab_score(abroot)
    total_nodenum = self.num_pruned + self.num_calculated
    ratio = self.num_calculated / total_nodenum * 100
    dprint(debug, "計算したノードの数",  self.num_calculated)
    dprint(debug, "枝狩りしたノードの数",  self.num_pruned)
    dprint(debug, "合計",  total_nodenum)
    dprint(debug, f"割合 {ratio:.1f}%")
    
Mbtree.calc_score_by_ab = calc_score_by_ab
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
from marubatsu import Marubatsu

def calc_score_by_ab(self, abroot, debug=False):    
元と同じなので省略
                    self.nodelist_by_score.append(node)
-                   self.ablist_by_score.append((alpha, beta, score, "score",
-                                               self.num_calculated, self.num_pruned))
                    if score >= beta:
                        index = node.children.index(childnode)
                        for prunednode in node.children[index + 1:]:
                            assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
-                           break
+                   self.ablist_by_score.append((alpha, beta, None, "update",
+                                                self.num_calculated, self.num_pruned))
+                   if score >= beta:
+                       break
元と同じなので省略
                    self.nodelist_by_score.append(node)
-                   self.ablist_by_score.append((alpha, beta, None, "update",
-                                                self.num_calculated, self.num_pruned))
                    if score <= alpha:
                        index = node.children.index(childnode)
                        for prunednode in node.children[index + 1:]:
                            assign_pruned_index(prunednode, len(self.nodelist_by_score) - 1)
-                           break
+                   self.ablist_by_score.append((alpha, beta, None, "update",
+                                                self.num_calculated, self.num_pruned))
+                   if score <= alpha:
+                       break
元と同じなので省略
    
Mbtree.calc_score_by_ab = calc_score_by_ab
```
</details>

上記の修正後に下記のプログラムを実行して正しい処理が行われるようになったかどうかを確認します。

```python
mbtree.calc_score_by_ab(mbtree.root)
Mbtree_Anim(mbtree, isscore=True)
```

下図は β 狩りの処理が行われた 4486 フレーム目の図で、先程と異なり __枝狩りの数が正しく 14694 に増えている__ ことが確認できます。また、このフレームでは __ノードの評価値が確定しない__ ので __計算済の数は 1121 のまま増えていない__ ことも確認できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/524c5eb7-5eb7-90d6-3620-3d7ac421debd.png" width="450">

下図はこのノードの評価値が確定したその次 4487 フレーム目の図で、__計算済の数が正しく 1121 から 1122 に 1 だけ増えている__ ことが確認できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/43f5d674-0a03-c958-8e78-ef0a816526fe.png" width="450">

# フレーム間の差分データの表示

枝狩りの処理が行われた際に、枝狩りによって処理が省略されたノードの数を表示する方法として、本記事では __直前のフレーム__ に表示されていた「枝狩り」の数と __現在のフレーム__ で表示されている数の __差分を表示__ するという方法を取ることにします。また、その際にせっかくなので「計算済」、「合計」などの差分も表示することにします。もっと良い表示方法を思いついた方は実装してみて下さい。

直前のフレームとの差分を計算するためには、__直前のフレームの番号を記録しておく必要がある__ ので、その値を __`prev_frame` という属性に記録する__ ことにします。

## Mbtree_Anim クラスの `__init__` メソッドの修正

__`prev_frame`__ 属性を導入する場合は、その __初期化処理を行う必要__ があります。Mbtree_Anim で __フレームが最初に表示__ された際には __直前のフレームは存在しない__ ので、`prev_frame` を `None` で初期化するという方法が考えられますが、__`None` で初期化を行う__ と差分の計算処理を `prev_frame` が __`None` の場合とそうでない場合で分けて行う必要がある点が面倒__ です。

このような場合は、__`prev_frame` 属性の初期値__ として、__最初に表示されるフレームである `0` を設定__ するのが一般的だと思います。そのように初期化することで、__最初に表示を行った際の差分がすべて 0 で計算される__ ことになります。

__`prev_frame` 属性の初期化処理__ は __`__init__` メソッドで行えば良い__ ので、下記のプログラムの 2 行目のように `__init__` メソッドを修正します。

```python
1  def __init__(self, mbtree:Mbtree, isscore:bool=False, size:float=0.15):
元と同じなので省略
2      self.prev_frame = 0
3      super(Mbtree_Anim, self).__init__()
4      
5  Mbtree_Anim.__init__ = __init__
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def __init__(self, mbtree:Mbtree, isscore:bool=False, size:float=0.15):
    self.mbtree = mbtree
    self.isscore = isscore
    self.size = size
    self.width = 50
    self.height = 65
    self.nodelist = self.mbtree.nodelist_by_score if isscore else self.mbtree.nodelist 
    self.nodenum = len(self.nodelist)
    self.prev_frame = 0
    super(Mbtree_Anim, self).__init__()
    
Mbtree_Anim.__init__ = __init__
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def __init__(self, mbtree:Mbtree, isscore:bool=False, size:float=0.15):
元と同じなので省略
+   self.prev_frame = 0
    super(Mbtree_Anim, self).__init__()
    
Mbtree_Anim.__init__ = __init__
```
</details>

## Mbtree_Anim クラスの `update_ab` メソッドの修正

次に、__`update_ab`__ メソッドを下記のプログラムのように、__直前のフレームとの差分を表示__ するように修正します。なお、直前のフレームを表す __`prev_frame`__ 属性には `__init__` メソッドで __`0` が代入__ されており、__その値を変更する処理をまだ記述していない__ ので、現時点では __常に最初の `0` フレーム目との差分が表示__ されます。`prev_frame` 属性の更新の処理はこの後で記述します。

* __6 行目__：直前のフレームのデータから、[A と P](#mbtree_anim-のバグの修正と改良) の値を取り出して変数に代入する。変数の名前は現在のフレームの A と P を代入する変数の前に `prev_` をつけた名前とした。また差分を計算する際に必要のないデータは `_` という変数に代入した
* __7 行目__：直前のフレームの M を計算する
* __8 ~ 11 行目__：現在のフレームと直前のフレームの A、P、M の差分を計算して変数に代入する。また、__R__ としては __M の差分に対する A の差分の比率__ を計算することにした
* __15 行目__：上から表示する順で、差分を要素とする list を `diff_datalist` に代入する
* __19 行目__：18 行目と同様の方法で差分のデータを右揃えで描画する

```python
 1  import matplotlib.patches as patches
 2  
 3  def update_ab(self):
元と同じなので省略
 4      num_total = num_calculated + num_pruned
 5      num_ratio = num_calculated / num_total if num_total != 0 else 0
 6      _, _, _, _, prev_num_calculated, prev_num_pruned = self.mbtree.ablist_by_score[self.prev_frame]
 7      prev_num_total = prev_num_calculated + prev_num_pruned
 8      diff_num_calculated = num_calculated - prev_num_calculated
 9      diff_num_pruned = num_pruned - prev_num_pruned
10      diff_num_total = num_total - prev_num_total
11      diff_num_ratio = diff_num_calculated / diff_num_total if diff_num_total != 0 else 0
12  
13      textlist = [ "計算済", "枝狩り", "合計", "割合" ]
14      datalist = [ num_calculated, num_pruned, num_total, f"{num_ratio * 100:.1f}%"]
15      diff_datalist = [ diff_num_calculated, diff_num_pruned, diff_num_total, f"{diff_num_ratio * 100:.1f}%"]
16      for i in range(4):
17          self.abax.text(15, 1 - i * 0.7, textlist[i])
18          self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
19          self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
20  
21  Mbtree_Anim.update_ab = update_ab
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
import matplotlib.patches as patches

def update_ab(self):
    alpha, beta, score, status, num_calculated, num_pruned = self.mbtree.ablist_by_score[self.play.value]
    maxnode = self.selectednode.mb.turn == Marubatsu.CIRCLE
    acolor = "red" if maxnode else "black"
    bcolor = "black" if maxnode else "red"
                            
    self.abax.clear()
    self.abax.set_xlim(-4, 23)
    self.abax.set_ylim(-1.5, 1.5)
    self.abax.axis("off")

    minus_inf = -3
    plus_inf = 4   
    alphacoord = max(minus_inf, alpha)
    betacoord = min(plus_inf, beta)
    
    color = "lightgray" if maxnode else "aqua"
    rect = patches.Rectangle(xy=(minus_inf, -0.5), width=alphacoord-minus_inf,
                            height=1, fc=color)
    self.abax.add_patch(rect)
    rect = patches.Rectangle(xy=(alphacoord, -0.5), width=betacoord-alphacoord,
                            height=1, fc="yellow")
    self.abax.add_patch(rect)
    color = "aqua" if maxnode else "lightgray"
    rect = patches.Rectangle(xy=(betacoord, -0.5), width=plus_inf-betacoord,
                            height=1, fc=color)
    self.abax.add_patch(rect)

    self.abax.plot(range(minus_inf, plus_inf + 1), [0] * (plus_inf + 1 - minus_inf) , "|-k")
    for num in range(minus_inf, plus_inf + 1):
        if num == minus_inf:
            numtext = "-∞"
        elif num == plus_inf:
            numtext = "∞"
        else:
            numtext = num
        self.abax.text(num, -1, numtext, ha="center")
        
    arrowprops = { "arrowstyle": "->"}
    self.abax.plot(alphacoord, 0, "or")
    self.abax.annotate(f"α = {alpha}", xy=(alphacoord, 0), xytext=(minus_inf, 1),
                    arrowprops=arrowprops, ha="center", c=acolor)
    self.abax.plot(betacoord, 0, "ob")
    self.abax.annotate(f"β = {beta}", xy=(betacoord, 0), xytext=(plus_inf, 1),
                    arrowprops=arrowprops, ha="center", c=bcolor)
    if score is not None:
        self.abax.plot(score, 0, "og")
        self.abax.annotate(f"score = {score}", xy=(score, 0),
                        xytext=((minus_inf + plus_inf) / 2, 1), 
                        arrowprops=arrowprops, ha="center")
            
    facecolorlist = ["aqua", "yellow", "lightgray"]
    textcolorlist = ["black", "black", "black"]
    if maxnode:
        nodetype = f"深さ {self.selectednode.mb.move_count} max node"
        textlist = ["β 狩り (β ≦ score)", "α 値の更新 (α < score < β)", "α 値の更新なし (score ≦ α)"]
        if score is not None :
            if beta <= score:
                textcolorlist[0] = "red"
            elif alpha < score:
                textcolorlist[1] = "red"
            else:
                textcolorlist[2] = "red"
    else:
        nodetype = f"深さ {self.selectednode.mb.move_count} min node"
        textlist = ["α 狩り (score <= α)", "β 値の更新 (α < score < β)", "β 値の更新なし (score ≦ β)"]
        if score is not None :
            if score <= alpha:
                textcolorlist[0] = "red"
            elif score < beta:
                textcolorlist[1] = "red"
            else:
                textcolorlist[2] = "red"
            
    if status == "start":
        facecolor = "white"
        nodetype += " 処理の開始"
    elif status == "score":
        facecolor = "lightyellow"
        nodetype += " 子ノードの評価値"
    elif status == "update":
        facecolor = "lightcyan"
        if maxnode:
            nodetype += " α 値の処理"
        else:
            nodetype += " β 値の処理"
    else:
        facecolor = "lavenderblush"
        nodetype += " 評価値の確定"
    self.abfig.set_facecolor(facecolor)
    self.abax.text(6, 1, nodetype)   
    for i in range(3):
        rect = patches.Rectangle(xy=(5, 0.3 - i * 0.7), width=0.8, height=0.5, fc=facecolorlist[i])
        self.abax.add_patch(rect)
        self.abax.text(6, 0.4 - i * 0.7, textlist[i], c=textcolorlist[i])    
    
    num_total = num_calculated + num_pruned
    num_ratio = num_calculated / num_total if num_total != 0 else 0
    _, _, _, _, prev_num_calculated, prev_num_pruned = self.mbtree.ablist_by_score[self.prev_frame]
    prev_num_total = prev_num_calculated + prev_num_pruned
    diff_num_calculated = num_calculated - prev_num_calculated
    diff_num_pruned = num_pruned - prev_num_pruned
    diff_num_total = num_total - prev_num_total
    diff_num_ratio = diff_num_calculated / diff_num_total if diff_num_total != 0 else 0

    textlist = [ "計算済", "枝狩り", "合計", "割合" ]
    datalist = [ num_calculated, num_pruned, num_total, f"{num_ratio * 100:.1f}%"]
    diff_datalist = [ diff_num_calculated, diff_num_pruned, diff_num_total, f"{diff_num_ratio * 100:.1f}%"]
    for i in range(4):
        self.abax.text(15, 1 - i * 0.7, textlist[i])
        self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
        self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")

Mbtree_Anim.update_ab = update_ab
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
import matplotlib.patches as patches

def update_ab(self):
元と同じなので省略
    num_total = num_calculated + num_pruned
    num_ratio = num_calculated / num_total if num_total != 0 else 0
+   _, _, _, _, prev_num_calculated, prev_num_pruned = self.mbtree.ablist_by_score[self.prev_frame]
+   prev_num_total = prev_num_calculated + prev_num_pruned
+   diff_num_calculated = num_calculated - prev_num_calculated
+   diff_num_pruned = num_pruned - prev_num_pruned
+   diff_num_total = num_total - prev_num_total
+   diff_num_ratio = diff_num_calculated / diff_num_total if diff_num_total != 0 else 0

    textlist = [ "計算済", "枝狩り", "合計", "割合" ]
    datalist = [ num_calculated, num_pruned, num_total, f"{num_ratio * 100:.1f}%"]
+   diff_datalist = [ diff_num_calculated, diff_num_pruned, diff_num_total, f"{diff_num_ratio * 100:.1f}%"]
    for i in range(4):
        self.abax.text(15, 1 - i * 0.7, textlist[i])
        self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
+       self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")

Mbtree_Anim.update_ab = update_ab
```
</details>

上記の修正後に下記のプログラムを実行し、選択中のノード内の移動の右の > ボタンをクリックして 9351 フレーム目を表示すると、実行結果のように一番右の列に 0 フレーム目との差分の A、P、M と差分の割合のデータが表示されるようになります。なお、最初の 0 フレーム目の A、P、M はいずれも 0 なので、__差分として表示されるデータ__ はいずれも __現在のフレームのデータと同じ__ になります。他のフレームの表示も確認してみて下さい。

```python
Mbtree_Anim(mbtree, isscore=True)
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/478eee34-2cfd-e5a9-2395-eb06fdeb8cd0.png" width="500">

### 差分のデータであることの明確化

上図では差分として「2338」のような正の数値が表示されていますが、__差分が正の値であった場合でも「+2338」__ のように __符号を表示__ することで、2338 だけ __増加した差分のデータであることが明確になります__。従って、__差分のデータを表示する場合__ は __常に符号を表示したほうがわかりやすい__ でしょう。

数値に常に符号をつけて表示するには、[以前の記事](https://qiita.com/ysgeso/items/bf42b7b578bc2d5da949#%E7%AC%A6%E5%8F%B7)で説明した __f 文字列__ の __書式指定__ に __+__ を記述します。具体的には、下記のプログラムのように `update_ab` メソッドを修正します。

* __2 行目__：__`diff_datalist`__ の A、P、M の要素を __f 文字列の書式指定__ に __`+d`__ を記述することで、__符号がついた整数の文字列に変換__ する。書式指定の [+](https://qiita.com/ysgeso/items/bf42b7b578bc2d5da949#%E7%AC%A6%E5%8F%B7) が符合をつける、[d](https://qiita.com/ysgeso/items/bf42b7b578bc2d5da949#%E8%A1%A8%E7%8F%BE%E5%9E%8B) が整数の文字列に変換することを表す

```python
 1  def update_ab(self):
元と同じなので省略
 2      diff_datalist = [ f"{diff_num_calculated:+d}", f"{diff_num_pruned:+d}", 
 3                        f"{diff_num_total:+d}", f"{diff_num_ratio * 100:.1f}%"]
 4      for i in range(4):
 5          self.abax.text(15, 1 - i * 0.7, textlist[i])
 6          self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
 7          self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
 8  
 9  Mbtree_Anim.update_ab = update_ab
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def update_ab(self):
    alpha, beta, score, status, num_calculated, num_pruned = self.mbtree.ablist_by_score[self.play.value]
    maxnode = self.selectednode.mb.turn == Marubatsu.CIRCLE
    acolor = "red" if maxnode else "black"
    bcolor = "black" if maxnode else "red"
                            
    self.abax.clear()
    self.abax.set_xlim(-4, 23)
    self.abax.set_ylim(-1.5, 1.5)
    self.abax.axis("off")

    minus_inf = -3
    plus_inf = 4   
    alphacoord = max(minus_inf, alpha)
    betacoord = min(plus_inf, beta)
    
    color = "lightgray" if maxnode else "aqua"
    rect = patches.Rectangle(xy=(minus_inf, -0.5), width=alphacoord-minus_inf,
                            height=1, fc=color)
    self.abax.add_patch(rect)
    rect = patches.Rectangle(xy=(alphacoord, -0.5), width=betacoord-alphacoord,
                            height=1, fc="yellow")
    self.abax.add_patch(rect)
    color = "aqua" if maxnode else "lightgray"
    rect = patches.Rectangle(xy=(betacoord, -0.5), width=plus_inf-betacoord,
                            height=1, fc=color)
    self.abax.add_patch(rect)

    self.abax.plot(range(minus_inf, plus_inf + 1), [0] * (plus_inf + 1 - minus_inf) , "|-k")
    for num in range(minus_inf, plus_inf + 1):
        if num == minus_inf:
            numtext = "-∞"
        elif num == plus_inf:
            numtext = "∞"
        else:
            numtext = num
        self.abax.text(num, -1, numtext, ha="center")
        
    arrowprops = { "arrowstyle": "->"}
    self.abax.plot(alphacoord, 0, "or")
    self.abax.annotate(f"α = {alpha}", xy=(alphacoord, 0), xytext=(minus_inf, 1),
                    arrowprops=arrowprops, ha="center", c=acolor)
    self.abax.plot(betacoord, 0, "ob")
    self.abax.annotate(f"β = {beta}", xy=(betacoord, 0), xytext=(plus_inf, 1),
                    arrowprops=arrowprops, ha="center", c=bcolor)
    if score is not None:
        self.abax.plot(score, 0, "og")
        self.abax.annotate(f"score = {score}", xy=(score, 0),
                        xytext=((minus_inf + plus_inf) / 2, 1), 
                        arrowprops=arrowprops, ha="center")
            
    facecolorlist = ["aqua", "yellow", "lightgray"]
    textcolorlist = ["black", "black", "black"]
    if maxnode:
        nodetype = f"深さ {self.selectednode.mb.move_count} max node"
        textlist = ["β 狩り (β ≦ score)", "α 値の更新 (α < score < β)", "α 値の更新なし (score ≦ α)"]
        if score is not None :
            if beta <= score:
                textcolorlist[0] = "red"
            elif alpha < score:
                textcolorlist[1] = "red"
            else:
                textcolorlist[2] = "red"
    else:
        nodetype = f"深さ {self.selectednode.mb.move_count} min node"
        textlist = ["α 狩り (score <= α)", "β 値の更新 (α < score < β)", "β 値の更新なし (score ≦ β)"]
        if score is not None :
            if score <= alpha:
                textcolorlist[0] = "red"
            elif score < beta:
                textcolorlist[1] = "red"
            else:
                textcolorlist[2] = "red"
            
    if status == "start":
        facecolor = "white"
        nodetype += " 処理の開始"
    elif status == "score":
        facecolor = "lightyellow"
        nodetype += " 子ノードの評価値"
    elif status == "update":
        facecolor = "lightcyan"
        if maxnode:
            nodetype += " α 値の処理"
        else:
            nodetype += " β 値の処理"
    else:
        facecolor = "lavenderblush"
        nodetype += " 評価値の確定"
    self.abfig.set_facecolor(facecolor)
    self.abax.text(6, 1, nodetype)   
    for i in range(3):
        rect = patches.Rectangle(xy=(5, 0.3 - i * 0.7), width=0.8, height=0.5, fc=facecolorlist[i])
        self.abax.add_patch(rect)
        self.abax.text(6, 0.4 - i * 0.7, textlist[i], c=textcolorlist[i])    
    
    num_total = num_calculated + num_pruned
    num_ratio = num_calculated / num_total if num_total != 0 else 0
    _, _, _, _, prev_num_calculated, prev_num_pruned = self.mbtree.ablist_by_score[self.prev_frame]
    prev_num_total = prev_num_calculated + prev_num_pruned
    diff_num_calculated = num_calculated - prev_num_calculated
    diff_num_pruned = num_pruned - prev_num_pruned
    diff_num_total = num_total - prev_num_total
    diff_num_ratio = diff_num_calculated / diff_num_total if diff_num_total != 0 else 0

    textlist = [ "計算済", "枝狩り", "合計", "割合" ]
    datalist = [ num_calculated, num_pruned, num_total, f"{num_ratio * 100:.1f}%"]
    diff_datalist = [ f"{diff_num_calculated:+d}", f"{diff_num_pruned:+d}", 
                      f"{diff_num_total:+d}", f"{diff_num_ratio * 100:.1f}%"]
    for i in range(4):
        self.abax.text(15, 1 - i * 0.7, textlist[i])
        self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
        self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")

Mbtree_Anim.update_ab = update_ab
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def update_ab(self):
元と同じなので省略
-   diff_datalist = [ diff_num_calculated, diff_num_pruned, 
-                     diff_num_total, f"{diff_num_ratio * 100:.1f}%"]
+   diff_datalist = [ f"{diff_num_calculated:+d}", f"{diff_num_pruned:+d}", 
+                     f"{diff_num_total:+d}", f"{diff_num_ratio * 100:.1f}%"]
    for i in range(4):
        self.abax.text(15, 1 - i * 0.7, textlist[i])
        self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
        self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")

Mbtree_Anim.update_ab = update_ab
```
</details>

上記の修正後に下記のプログラムを実行すると、実行結果のように __差分の数値に符号が表示__ されるようになったこと確認できます。

```python
Mbtree_Anim(mbtree, isscore=True)
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/7bff5071-a131-3a87-119d-f5dac326d4d1.png" width="500">

## 直前のフレームの情報の更新方法

次に、直前のフレームを表す __`prev_frame`__ 属性を __更新する処理を記述__ する必要がありますが、どのように記述すれば良いかについて少し考えてみて下さい。

### 間違った更新方法

ぱっと思いつく方法として、__`update_ab`__ メソッド内の __描画の更新を行う処理を行った直後__ に __`prev_frame`__ 属性を現在のアニメーションのフレームを表す __`self.play.value` で更新する__ という方法があるでしょう。

この方法の考え方は以下の通りです。

* __フレームが変更__ されると __`update_ab` が呼び出されて描画の更新を行われる__
* `update_ab` 内で __描画の更新を行う処理を行った後__ で __`prev_frame` を現在のフレームに更新__ すると、__次にアニメーションのフレームが変更__ されて `update_ab` が呼び出された際に、__`prev_frame` には直前のフレームの値が代入__ されていることになる

一見するとこの方法で問題がないように見えるかもしれませんが、この方法には問題があります。__筆者も最初はこの方法を思いついて実際に実装を行ってしまいました__ ので、この方法の問題について説明することにします。

下記は、そのように __`update_ab`__ を修正したプログラムです。

__6 行目__：描画を更新した後で `prev_frame` に現在のアニメーションのフレームを表す `self.play.value` を代入して更新する

```python
1  def update_ab(self):
元と同じなので省略
2      for i in range(4):
3          self.abax.text(15, 1 - i * 0.7, textlist[i])
4          self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
5          self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
6      self.prev_frame = self.play.value
7
8  Mbtree_Anim.update_ab = update_ab
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def update_ab(self):
    alpha, beta, score, status, num_calculated, num_pruned = self.mbtree.ablist_by_score[self.play.value]
    maxnode = self.selectednode.mb.turn == Marubatsu.CIRCLE
    acolor = "red" if maxnode else "black"
    bcolor = "black" if maxnode else "red"
                            
    self.abax.clear()
    self.abax.set_xlim(-4, 23)
    self.abax.set_ylim(-1.5, 1.5)
    self.abax.axis("off")

    minus_inf = -3
    plus_inf = 4   
    alphacoord = max(minus_inf, alpha)
    betacoord = min(plus_inf, beta)
    
    color = "lightgray" if maxnode else "aqua"
    rect = patches.Rectangle(xy=(minus_inf, -0.5), width=alphacoord-minus_inf,
                            height=1, fc=color)
    self.abax.add_patch(rect)
    rect = patches.Rectangle(xy=(alphacoord, -0.5), width=betacoord-alphacoord,
                            height=1, fc="yellow")
    self.abax.add_patch(rect)
    color = "aqua" if maxnode else "lightgray"
    rect = patches.Rectangle(xy=(betacoord, -0.5), width=plus_inf-betacoord,
                            height=1, fc=color)
    self.abax.add_patch(rect)

    self.abax.plot(range(minus_inf, plus_inf + 1), [0] * (plus_inf + 1 - minus_inf) , "|-k")
    for num in range(minus_inf, plus_inf + 1):
        if num == minus_inf:
            numtext = "-∞"
        elif num == plus_inf:
            numtext = "∞"
        else:
            numtext = num
        self.abax.text(num, -1, numtext, ha="center")
        
    arrowprops = { "arrowstyle": "->"}
    self.abax.plot(alphacoord, 0, "or")
    self.abax.annotate(f"α = {alpha}", xy=(alphacoord, 0), xytext=(minus_inf, 1),
                    arrowprops=arrowprops, ha="center", c=acolor)
    self.abax.plot(betacoord, 0, "ob")
    self.abax.annotate(f"β = {beta}", xy=(betacoord, 0), xytext=(plus_inf, 1),
                    arrowprops=arrowprops, ha="center", c=bcolor)
    if score is not None:
        self.abax.plot(score, 0, "og")
        self.abax.annotate(f"score = {score}", xy=(score, 0),
                        xytext=((minus_inf + plus_inf) / 2, 1), 
                        arrowprops=arrowprops, ha="center")
            
    facecolorlist = ["aqua", "yellow", "lightgray"]
    textcolorlist = ["black", "black", "black"]
    if maxnode:
        nodetype = f"深さ {self.selectednode.mb.move_count} max node"
        textlist = ["β 狩り (β ≦ score)", "α 値の更新 (α < score < β)", "α 値の更新なし (score ≦ α)"]
        if score is not None :
            if beta <= score:
                textcolorlist[0] = "red"
            elif alpha < score:
                textcolorlist[1] = "red"
            else:
                textcolorlist[2] = "red"
    else:
        nodetype = f"深さ {self.selectednode.mb.move_count} min node"
        textlist = ["α 狩り (score <= α)", "β 値の更新 (α < score < β)", "β 値の更新なし (score ≦ β)"]
        if score is not None :
            if score <= alpha:
                textcolorlist[0] = "red"
            elif score < beta:
                textcolorlist[1] = "red"
            else:
                textcolorlist[2] = "red"
            
    if status == "start":
        facecolor = "white"
        nodetype += " 処理の開始"
    elif status == "score":
        facecolor = "lightyellow"
        nodetype += " 子ノードの評価値"
    elif status == "update":
        facecolor = "lightcyan"
        if maxnode:
            nodetype += " α 値の処理"
        else:
            nodetype += " β 値の処理"
    else:
        facecolor = "lavenderblush"
        nodetype += " 評価値の確定"
    self.abfig.set_facecolor(facecolor)
    self.abax.text(6, 1, nodetype)   
    for i in range(3):
        rect = patches.Rectangle(xy=(5, 0.3 - i * 0.7), width=0.8, height=0.5, fc=facecolorlist[i])
        self.abax.add_patch(rect)
        self.abax.text(6, 0.4 - i * 0.7, textlist[i], c=textcolorlist[i])    
    
    num_total = num_calculated + num_pruned
    num_ratio = num_calculated / num_total if num_total != 0 else 0
    _, _, _, _, prev_num_calculated, prev_num_pruned = self.mbtree.ablist_by_score[self.prev_frame]
    prev_num_total = prev_num_calculated + prev_num_pruned
    diff_num_calculated = num_calculated - prev_num_calculated
    diff_num_pruned = num_pruned - prev_num_pruned
    diff_num_total = num_total - prev_num_total
    diff_num_ratio = diff_num_calculated / diff_num_total if diff_num_total != 0 else 0

    textlist = [ "計算済", "枝狩り", "合計", "割合" ]
    datalist = [ num_calculated, num_pruned, num_total, f"{num_ratio * 100:.1f}%"]
    diff_datalist = [ f"{diff_num_calculated:+d}", f"{diff_num_pruned:+d}", 
                      f"{diff_num_total:+d}", f"{diff_num_ratio * 100:.1f}%"]
    for i in range(4):
        self.abax.text(15, 1 - i * 0.7, textlist[i])
        self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
        self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
    self.prev_frame = self.play.value

Mbtree_Anim.update_ab = update_ab
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def update_ab(self):
元と同じなので省略
    for i in range(4):
        self.abax.text(15, 1 - i * 0.7, textlist[i])
        self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
        self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
+   self.prev_frame = self.play.value

Mbtree_Anim.update_ab = update_ab
```
</details>

上記の修正後に下記のプログラムを実行し、選択中のノード内の移動の右の > ボタンをクリックして 9351 フレーム目を表示すると、実行結果のように __差分のデータにすべて +0 が表示される__ という問題が発生することがわかります。また、他のボタンをクリックして __別のフレームを表示__ しても __同様に差分のデータにすべて +0 が表示される__ ことを実際に確認してみて下さい。また、この問題の原因について少し考えてみて下さい。

```python
Mbtree_Anim(mbtree, isscore=True)
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/0ec056a3-6b97-cb76-b941-ced2cb012dd2.png" width="500">

### バグの原因

この問題の原因は直前のフレームを表す __`prev_frame` 属性の値の更新__ を、__描画の更新を行う `update_ab` メソッドの中で行っている__ 点にあります。そのため __`update_ab`__ メソッドが、アニメーションのフレームが __変更されていない場合に呼び出される__ と、__`prev_frame` が直前のフレームではなくなってしまいます__。

わかりづらいと思いますので、具体例として __0 フレーム目から 100 フレーム目、200 フレームに表示が変更__ された場合の処理を説明します。

下記の表は、アニメーションの __フレームが変更された際__ に、__`update_ab`__ が __1 回だけ__ 呼び出される場合の処理を表します。表の __太字の行__ のように `update_ab` で表示を更新する際に __`prev_frame` には直前のフレームの値が代入されている__ ので、差分が正しく描画されます。

|  | 現在のフレーム | `prev_frame` の値 |
|:--|--:|--:|
| 100 フレームが変更される前  | 0  | 0  |
| 100 フレーム目に表示が移動  | 100  | 0  |
| __`update_ab` での表示の更新中__  | __100__  | __0__  |
| `update_ab` の処理の終了後  | 100  | 100  |
| 200 フレーム目に表示が移動  | 200  | 100  |
| __`update_ab` での表示の更新中__  | __200__  | __100__  |
| `update_ab` の処理の終了後  | 200  | 200  |

一方、下記の表はアニメーションの __フレームが変更された際__ に、__`update_ab`__ が __2 回__ 呼び出された場合の処理を表します。__1 回目の `update_ab`__ の処理によって __`prev_frame`__ の値が __現在のアニメーションのフレームに更新__ されたしまうため、__太字の行__ のように __2 回目の `update_ab` で表示を更新する際__ には __`prev_frame` には現在のフレームが代入__ されています。そのため、__差分には必ず 0 が表示されます__。

|  | 現在のフレーム | `prev_frame` の値 |
|:--|--:|--:|
| 100 フレームが変更される前  | 0  | 0  |
| 100 フレーム目に表示が移動  | 100  | 0  |
| 1 回目の `update_ab` での表示の更新中  | 100  | 0  |
| 1 回目の `update_ab` の処理の終了後  | 100  | 100  |
| __2 回目の `update_ab` での表示の更新中__  | __100__  | __100__  |
| 2 回目の `update_ab` の処理の終了後  | 100  | 100  |
| 200 フレーム目に表示が移動  | 200  | 100  |
| 1 回目の `update_ab` での表示の更新中  | 200  | 100  |
| 1 回目の `update_ab` の処理の終了後  | 200  | 200  |
| __2 回目の `update_ab` での表示の更新中__  | __200__  | __200__  |
| 2 回目の `update_ab` の処理の終了後  | 200  | 200  |


### `update_ab` が呼び出された回数の確認

上記では、フレームが変更された際に `update_ab` が 2 回以上呼び出されると差分に 0 が表示されることを示しましたが、フレームが変更された際に `update_ab` が何回呼び出されているの確認は行っていません。

そこで、フレームが変更された際に __本当に `update_ab` が 2 回以上呼び出されているかを確認__ するために、下記のプログラムの 3 行目のように __`update_ab`__ の中で __`prev_frame` 属性の値を更新__ した際に __update_ab というメッセージを表示__ するように修正します。 

```python
1  def update_ab(self):
元と同じなので省略
2      self.prev_frame = self.play.value
3      print("update_ab")
4
5  Mbtree_Anim.update_ab = update_ab
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def update_ab(self):
    alpha, beta, score, status, num_calculated, num_pruned = self.mbtree.ablist_by_score[self.play.value]
    maxnode = self.selectednode.mb.turn == Marubatsu.CIRCLE
    acolor = "red" if maxnode else "black"
    bcolor = "black" if maxnode else "red"
                            
    self.abax.clear()
    self.abax.set_xlim(-4, 23)
    self.abax.set_ylim(-1.5, 1.5)
    self.abax.axis("off")

    minus_inf = -3
    plus_inf = 4   
    alphacoord = max(minus_inf, alpha)
    betacoord = min(plus_inf, beta)
    
    color = "lightgray" if maxnode else "aqua"
    rect = patches.Rectangle(xy=(minus_inf, -0.5), width=alphacoord-minus_inf,
                            height=1, fc=color)
    self.abax.add_patch(rect)
    rect = patches.Rectangle(xy=(alphacoord, -0.5), width=betacoord-alphacoord,
                            height=1, fc="yellow")
    self.abax.add_patch(rect)
    color = "aqua" if maxnode else "lightgray"
    rect = patches.Rectangle(xy=(betacoord, -0.5), width=plus_inf-betacoord,
                            height=1, fc=color)
    self.abax.add_patch(rect)

    self.abax.plot(range(minus_inf, plus_inf + 1), [0] * (plus_inf + 1 - minus_inf) , "|-k")
    for num in range(minus_inf, plus_inf + 1):
        if num == minus_inf:
            numtext = "-∞"
        elif num == plus_inf:
            numtext = "∞"
        else:
            numtext = num
        self.abax.text(num, -1, numtext, ha="center")
        
    arrowprops = { "arrowstyle": "->"}
    self.abax.plot(alphacoord, 0, "or")
    self.abax.annotate(f"α = {alpha}", xy=(alphacoord, 0), xytext=(minus_inf, 1),
                    arrowprops=arrowprops, ha="center", c=acolor)
    self.abax.plot(betacoord, 0, "ob")
    self.abax.annotate(f"β = {beta}", xy=(betacoord, 0), xytext=(plus_inf, 1),
                    arrowprops=arrowprops, ha="center", c=bcolor)
    if score is not None:
        self.abax.plot(score, 0, "og")
        self.abax.annotate(f"score = {score}", xy=(score, 0),
                        xytext=((minus_inf + plus_inf) / 2, 1), 
                        arrowprops=arrowprops, ha="center")
            
    facecolorlist = ["aqua", "yellow", "lightgray"]
    textcolorlist = ["black", "black", "black"]
    if maxnode:
        nodetype = f"深さ {self.selectednode.mb.move_count} max node"
        textlist = ["β 狩り (β ≦ score)", "α 値の更新 (α < score < β)", "α 値の更新なし (score ≦ α)"]
        if score is not None :
            if beta <= score:
                textcolorlist[0] = "red"
            elif alpha < score:
                textcolorlist[1] = "red"
            else:
                textcolorlist[2] = "red"
    else:
        nodetype = f"深さ {self.selectednode.mb.move_count} min node"
        textlist = ["α 狩り (score <= α)", "β 値の更新 (α < score < β)", "β 値の更新なし (score ≦ β)"]
        if score is not None :
            if score <= alpha:
                textcolorlist[0] = "red"
            elif score < beta:
                textcolorlist[1] = "red"
            else:
                textcolorlist[2] = "red"
            
    if status == "start":
        facecolor = "white"
        nodetype += " 処理の開始"
    elif status == "score":
        facecolor = "lightyellow"
        nodetype += " 子ノードの評価値"
    elif status == "update":
        facecolor = "lightcyan"
        if maxnode:
            nodetype += " α 値の処理"
        else:
            nodetype += " β 値の処理"
    else:
        facecolor = "lavenderblush"
        nodetype += " 評価値の確定"
    self.abfig.set_facecolor(facecolor)
    self.abax.text(6, 1, nodetype)   
    for i in range(3):
        rect = patches.Rectangle(xy=(5, 0.3 - i * 0.7), width=0.8, height=0.5, fc=facecolorlist[i])
        self.abax.add_patch(rect)
        self.abax.text(6, 0.4 - i * 0.7, textlist[i], c=textcolorlist[i])    
    
    num_total = num_calculated + num_pruned
    num_ratio = num_calculated / num_total if num_total != 0 else 0
    _, _, _, _, prev_num_calculated, prev_num_pruned = self.mbtree.ablist_by_score[self.prev_frame]
    prev_num_total = prev_num_calculated + prev_num_pruned
    diff_num_calculated = num_calculated - prev_num_calculated
    diff_num_pruned = num_pruned - prev_num_pruned
    diff_num_total = num_total - prev_num_total
    diff_num_ratio = diff_num_calculated / diff_num_total if diff_num_total != 0 else 0

    textlist = [ "計算済", "枝狩り", "合計", "割合" ]
    datalist = [ num_calculated, num_pruned, num_total, f"{num_ratio * 100:.1f}%"]
    diff_datalist = [ f"{diff_num_calculated:+d}", f"{diff_num_pruned:+d}", 
                      f"{diff_num_total:+d}", f"{diff_num_ratio * 100:.1f}%"]
    for i in range(4):
        self.abax.text(15, 1 - i * 0.7, textlist[i])
        self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
        self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
    self.prev_frame = self.play.value
    print("update_ab")

Mbtree_Anim.update_ab = update_ab
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def update_ab(self):
元と同じなので省略
    self.prev_frame = self.play.value
+   print("update_ab")

Mbtree_Anim.update_ab = update_ab
```
</details>

上記の修正後に下記のプログラムを実行すると、画像の下に実行結果の 1 行目のように __update_ab が 1 回だけ表示__ されるので、__最初の描画を行うために `update_ab` が 1 回だけ呼び出された__ ことが確認できます。なお、実行結果の 3 行目は __Mbtree_Anim__ の呼び出しによって __返り値として返された Mbtree_Anim クラスのインスタンス__ を表します。

```python
Mbtree_Anim(mbtree, isscore=True)
```

実行結果

```
update_ab

<tree.Mbtree_Anim at 0x15b4feb9930>
```

その後で選択中のノード内の移動の右の > ボタンをクリックして __フレームを変更__ すると下記のように __update_ab が 2 回表示される__ ので、確かに > ボタンをクリックして __フレームを変更__ したことで  __`update_ab`__ メソッドが __2 回呼び出されている__ ことが確認できました。

```
update_ab
update_ab
```

__他のフレームを移動するボタンをクリック__ した際に、__同様に update_ab が 2 回表示される__ ことを確認して下さい。

上記から、差分に常に 0 が表示されるという __問題の原因__ が、__フレームが変更された際__ に、__`update_ab`__ が __2 回__ 呼び出されたことであることが確認できました。

### 間違った考え方をした原因

先程、この方法の考え方を以下のように説明しました。この考え方はフレームが変更された際に __`update_ab` が 1 回だけしか呼び出されない場合は正しい__ ですが、`update_ab` が __2 回以上__ 呼び出される場合は __正しくありません__。

* アニメーションのフレームが変更されると `update_ab` を呼び出して描画の更新を行う
* `update_ab` 内で描画の更新を行う処理を行った後で `prev_frame` を現在のフレームに更新すると、次にアニメーションのフレームが変更されて `update_ab` が呼び出された際に、`prev_frame` には直前のフレームの値が代入されていることになる

筆者が最初に上記の方法を考えたのは、フレームが変更された際に __`update_ab` を 1 回だけ__ 呼び出されるように __プログラムを記述したつもり__ だったからです。また、そのように __勘違いした理由__ は以下の通りです。

* Mbtree_Anim では __`update_ab`__ は __`update_gui` メソッドの中__ の処理で __1 度だけ__ 呼び出されるので、__`update_ab` が呼び出される回数__ は __`update_gui` が呼び出される回数と等しい__
* 例えば、__フレームを 1 つ先に移動する > ボタンをクリック__ した際の __イベントハンドラ__ は下記のプログラムのように定義されている。このプログラムから > ボタンをクリックすると、__`update_gui`__ メソッドが __1 回だけ呼び出される__ と考えた
* 従って > ボタンをクリックすると __`update_ab`__ メソッドが __1 回だけ呼び出される__ と考えた
* 他のフレームを移動するボタンに関しても同様だと考えた

```python
        def on_next_button_clicked(b=None):
            self.play.value += 1
            self.update_gui()
```

上記の筆者の考え方は間違っていますが、どこに間違いがあるかがわかる人は、それほど多くないと思います。

:::note warn
このような勘違いは、__他人が作ったモジュールを利用する際に良く起きます__。今回の場合は __ipywidgets__ モジュールを利用する際に __行われると筆者が思っていた処理__ が、__実際に行われている処理と異なっていた点__ が問題の原因です。
:::

### `update_ab` が複数回呼び出されてしまう原因の検証

ipywidgets のドキュメントをしっかりと読んで、ipywidgets モジュールが行う処理を理解すれば何を勘違いしているかを理解することは可能ですが、ドキュメントはかなり長く、__すべてを正しく理解するのはかなり困難__ です[^1]。そこで、フレームが変更された際に __`update_ab` が 2 回呼び出されてしまう原因__ を自力で検証することにします。

先ほど説明したように、Mbtree_Anim の処理の中で __`update_ab` メソッドを呼び出す処理が記述__ されているのは __`update_gui` メソッドの中だけ__ です。従って __`update_gui` メソッドが呼び出された場所を確認__ することで __`update_ab` メソッドが 2 回呼び出された原因を確認__ することができます。

__`update_gui`__ メソッドを呼び出す処理が記述されているのは __`create_event_handler` メソッドの中だけ__ なので、__`update_gui`__ メソッドを __呼び出す処理を行う直前__ に、下記のプログラムのように `print` で `update_gui` を呼び出した __ローカル関数の名前__ を表示するように `create_event_handler` メソッドを修正します。

* __3、8、13、17 行目__：`update_gui` を呼び出す直前に処理を行っているローカル関数の名前を表示する処理を追加する

```python
 1  def create_event_handler(self):
 2      def on_play_changed(changed):
 3          print("on_play_changed")
 4          self.update_gui()
 5              
 6      def on_prev_button_clicked(b=None):
 7          self.play.value -= 1
 8          print("on_prev_button_clicked")
 9          self.update_gui()
10          
11      def on_next_button_clicked(b=None):
12          self.play.value += 1
13          print("on_next_button_clicked")
14          self.update_gui()
元と同じなので省略
15      def change_frame(edge_status, diff, status_list):
元と同じなので省略
16          self.play.value = frame
17          print("change_frame")
18          self.update_gui()
元と同じなので省略
19    
20  Mbtree_Anim.create_event_handler = create_event_handler
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def create_event_handler(self):
    def on_play_changed(changed):
        print("on_play_changed")
        self.update_gui()
            
    def on_prev_button_clicked(b=None):
        self.play.value -= 1
        print("on_prev_button_clicked")
        self.update_gui()
        
    def on_next_button_clicked(b=None):
        self.play.value += 1
        print("on_next_button_clicked")
        self.update_gui()

    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)

    self.play.observe(on_play_changed, names="value")
    
    def change_frame(edge_status, diff, status_list):
        frame = self.play.value
        selectednode = self.mbtree.nodelist_by_score[frame]
        selectedstatus = self.mbtree.ablist_by_score[frame][3]
        if selectedstatus == edge_status:
            return
        while True:
            frame += diff
            node = self.mbtree.nodelist_by_score[frame]
            status = self.mbtree.ablist_by_score[frame][3]
            if node == selectednode and status in status_list:
                break
        self.play.value = frame
        print("change_frame")
        self.update_gui()
            
    def on_node_first_button_clicked(b=None):
        change_frame("start", -1, ["start"])
            
    def on_node_prev_button_clicked(b=None):
        change_frame("start", -1, ["start", "score"])

    def on_node_next_button_clicked(b=None):
        change_frame("end", 1, ["end", "score"])
        
    def on_node_last_button_clicked(b=None):
        change_frame("end", 1, ["end"])
    
    if self.abfig is not None:
        self.node_first_button.on_click(on_node_first_button_clicked)
        self.node_prev_button.on_click(on_node_prev_button_clicked)
        self.node_next_button.on_click(on_node_next_button_clicked)
        self.node_last_button.on_click(on_node_last_button_clicked)
    
Mbtree_Anim.create_event_handler = create_event_handler
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def create_event_handler(self):
    def on_play_changed(changed):
+       print("on_play_changed")
        self.update_gui()
            
    def on_prev_button_clicked(b=None):
        self.play.value -= 1
+       print("on_prev_button_clicked")
        self.update_gui()
        
    def on_next_button_clicked(b=None):
        self.play.value += 1
+       print("on_next_button_clicked")
        self.update_gui()
元と同じなので省略
    def change_frame(edge_status, diff, status_list):
元と同じなので省略
        self.play.value = frame
+       print("change_frame")
        self.update_gui()
元と同じなので省略
  
Mbtree_Anim.create_event_handler = create_event_handler
```
</details>

上記の修正後に下記のプログラムを実行した後で選択中のノード内の移動の右の > ボタンをクリックしてフレームを変更すると、実行結果のような表示が行われます。

```python
Mbtree_Anim(mbtree, isscore=True)
```

実行結果

```
on_play_changed
update_ab
change_frame
update_ab
```

実行結果から、ローカル関数 __`on_play_changed`__ と __`change_frame`__ の処理によって __`update_ab` が 2 回呼び出されている__ ことが確認できました。

下記は __他のフレームを移動するボタンを__ クリックした際に表示されるメッセージから、__呼び出されるローカル関数をまとめた表__ です。

|  | 1 回目に呼び出される関数 | 2 回目に呼び出される関数 |
|:-:|:-:|:-:|
| __上部の <__  | `on_play_changed`  | `on_prev_button_clicked`  |
| __上部の >__ |  `on_play_changed` | `on_next_button_clicked`  |
| __下部の <<__  |  `on_play_changed` |  `change_frame` |
| __下部の <__  |  `on_play_changed` |  `change_frame` |
| __下部の >__  |  `on_play_changed` |  `change_frame` |
| __下部の >>__  |  `on_play_changed` |  `change_frame` |

__1 回目の `update_ab`__ は __すべて `on_play_changed` から呼び出される__ ことがわかります。__下部の 4 つのボタン__ は下記のプログラムのように、いずれもそれらのボタンをクリックした際の __イベントハンドラ内__ で __`change_frame` を呼び出す__ ことでフレームを移動する処理を行っているので、__2 回目の `update_ab`__ は 上部のボタンも含めて __いずれも__ ボタンをクリックした際の __イベントハンドラから呼び出されている__ ことが確認できます。

```python
        def on_node_first_button_clicked(b=None):
            change_frame("start", -1, ["start"])
                
        def on_node_prev_button_clicked(b=None):
            change_frame("start", -1, ["start", "score"])

        def on_node_next_button_clicked(b=None):
            change_frame("end", 1, ["end", "score"])
            
        def on_node_last_button_clicked(b=None):
            change_frame("end", 1, ["end"])
```

フレームを 1 つ前に移動する __< ボタンをクリック__ した場合の処理を詳しく検証することにします。上記の表から、__`on_play_changed`__ と __`on_prev_button_clicked`__ の順番で __`update_gui` が呼び出された__ ことが確認できます。

下記はその 2 つの関数の定義で、__`on_play_changed`__ はフレームを表す __`self.play.value` の値が変更された場合__ に呼び出される関数です。__`on_prev_button_clicked`__ フレームを 1 つ前に移動する __< ボタンをクリックした場合__ に呼び出される関数です。

```python
1  def on_play_changed(changed):
2      print("on_play_changed")
3      self.update_gui()
4        
5  def on_prev_button_clicked(b=None):
6      self.play.value -= 1
7      print("on_prev_button_clicked")
8      self.update_gui()
```

このことから、< ボタンをクリックすると以下の理由で `update_gui` が 2 回呼び出されることが確認できました。

* __`on_prev_button_clicked`__ の __8 行目__ から `update_gui` が呼び出される
* __`on_prev_button_clicked`__ の 6 行目で __`self.play.value` の値が変更された結果__、__`on_play_changed` が呼び出され__、その 3 行目から `update_gui` が呼び出される

上記から、勘違いの原因が `self.play.value -= 1` という __一見すると値を代入する処理にしか見えない処理__ によって __`on_play_changed` が呼び出される__ ことに気が付かなかった点であることがわかります。

また、__`on_prev_button_clicked`__ または __`on_play_changed`__ の __どちらかのみから `update_gui` を呼び出す__ ようにプログラムを修正することでバグを修正することができますが、__そのような修正方法は行わないほうが良い__ でしょう。その理由について少し考えてみて下さい。

:::note warn
イベントハンドラのように、__何らかの処理に応じて後から別の関数を呼び出すような処理__ は、このような __思わぬ処理が行われる可能性がある__ 点に注意が必要です。
:::

:::note
< ボタンをクリックした際に、`on_prev_button_clicked` が最初に呼び出されるので、on_play_changed、on_prev_button_clicked の順で表示が行われる点がおかしいと思う人がいるかもしれないので補足します。

`on_play_changed` のように、ウィジェットの値が変更された際のイベントハンドラは、ウィジェットの [`observe`](https://qiita.com/ysgeso/items/17c74178449148ad3ef7#%E3%82%A6%E3%82%A3%E3%82%B8%E3%82%A7%E3%83%83%E3%83%88%E3%81%AE%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%83%8F%E3%83%B3%E3%83%89%E3%83%A9%E3%81%A8-observe-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89) メソッドによって結び付けます。`observe` メソッドによってウィジェットに結びつけられたイベントハンドラは、ウィジェットの値が変更された際にすぐに呼びだされるという性質があります。例えば、`self.play.value -= 1` のようにウィジェットの値を代入処理によって変更した場合は、その次の行の処理が行われる前に `on_play_changed` が呼び出されます。

従って、< ボタンをクリックした際には下記の手順で処理が行われます。

1. 6 行目が実行されて `self.play.value` の値が更新される
2. 次の 7 行目の処理が行われる前に `on_play_changed` が呼び出され、その中の 2 行目で on_play_changed が表示される
3. 7 行目が呼び出されて on_prev_button_clicked が表示される
:::

### `update_ab` 内で `prev_frame` を更新する方法の問題点

勘違いしている人が多いかもしれませんが、この __バグの原因は__ フレームが変更された際に __複数回 `update_ab` が呼ばれることではありません__。このバグの本当の原因は、__`prev_frame` を更新する処理__ を、__フレームを変更する処理ではない場所で行っている__ 点にあります。

__`update_ab`__ が行う処理は __描画の更新処理__ であり、その処理は現状の Mbtree_Anim が行っているように、__フレームが変更されない場合でも行われる可能性__ があります。

従って、フレームを変更するボタンをクリックした際に __`update_ab` が 1 回だけ呼び出されるように修正__ した場合は、__今後 Mbtree_Anim を修正した結果、別の場所から `update_ab` を呼び出す処理を記述__ してしまうと、__このバグが再び発生してしまう__ ことになります。

従って、__このバグを修正する正しい方法__ は、__`prev_frame` を更新する処理__ を、__フレームが更新された際に行う__ ようにすることです。どのように修正すれば良いかについて少し考えてみて下さい。

### IntSlider の操作によって行われる処理

正しい修正方法を説明する前に、フレームの移動を上部の IntSlider で行うこともできることに気が付きましたので、IntSlider でフレームを移動した場合のメッセージを確認することにします。この部分はおまけのようなものなので読み飛ばしても問題はありません。

IntSlider をドラッグしてフレームを移動した場合は、下記のように `on_play_changed` が 2 回呼ばれるようです。

```python
on_play_changed
update_ab
on_play_changed
update_ab
```

2 回呼ばれる原因は IntSlider のドラッグ操作がリアルタイムに `self.play.value` の値に反映され、そのたびに `on_play_changed` が呼び出されることです。そのため、ドラッグ操作をゆっくり行った場合は、3 回以上 `on_play_changed` が呼ばれることになります。

また、様々な速度でドラッグ操作を行ってみた結果、どれだけ素早くドラッグ操作を行っても最低でも 2 回は  `on_play_changed` が呼ばれることが判明しました。そのため、0 フレーム目からドラッグ操作を行った場合は、以下のような理由で下図のように差分の左の数値と差分の数値が一致しなくなるようです。

* ドラッグ操作を終えるまでの間に何度か `self.play.value` の値が更新される
* `self.play.value` の値が更新されるたびに `update_ab` が呼び出されて描画が更新される
* ドラッグ操作が完了した時点では、__ドラッグ操作の中で最後に描画が行われたアニメーションのフレームとの差分__ のデータが表示される

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/960ac9bf-1c4a-639c-87a6-1b519fe76837.png" width="500">

なお、IntSlider では、下記の方法でも値を変更することができ、その場合は `on_play_changed` は 1 回しか呼び出されないようです。

* IntSlider のボタンが表示されていない場所でクリックする
* IntSlider の右の数値をクリックし、キーボードから値を変更する

:::note
IntSlider の値を変更した際に行われる処理に時間がかかる場合は、ドラッグ操作の途中で `self.play.value` の値の変更が行われるたびにその処理が行われてしまうとプログラムの動作が非常に重くなってしまうという問題が発生します。そのような問題に対処するために、IntSlider などのウィジェットは、作成する際にキーワード属性 `continuous_update=False`[^2]を記述することで、ドラッグなどの操作が完了するまでの間に値を更新しないようにすることができます。

例えば、Mbtee_Anim の `create_widgets` の中で、アニメーションのフレームを変更する IntSlider のウィジェットを作成する処理を、下記のプログラムのように修正することで、IntSlider のドラッグ操作が完了するまで `on_play_changed` が呼び出されなくなります。


```python
self.frame_slider = widgets.IntSlider(max=self.nodenum - 1, description="frame",
                                      continuous_update=False)
```

なお、そのようにしてしまうとドラッグをゆっくり行うことで、ドラッグ中のフレームの画像をリアルタイムに見ることができなくなってしまうという欠点があるので、本記事では上記のように修正は行いません。興味がある方は実際に上記のように修正し、IntSlider のドラッグ中に表示が変わらなくなることを確認してみて下さい。
:::

### `prev_frame` を更新する場所

先程説明したように、__`prev_frame`__ 属性は __アニメーションのフレームが変更された際に更新する必要__ があります。ただし、そのことがわかっていても以下の理由から先程の間違った方法を最初に思いつく人が多いのではないかと思います。

* プログラムの中でアニメーションのフレームを表す `self.play.value` を変更する場所が複数ある
* その全ての場所で `prev_frame` 属性を更新する処理を記述するのは面倒
* アニメーションのフレームが変更された場合は、必ず `update_ab` で表示を更新するので、そちらに `prev_frame` 属性を更新する処理を記述すれば良いのではなかと勘違いする

先程の表からわかるように、アニメーションのフレームを表す __`self.play.value` の値が変更された場合__ は必ず __`on_play_chaned` が 1 回だけ呼び出される__ ので、__その中で `prev_frame` 属性を更新する処理を記述__ することができます。

また、[以前の記事](https://qiita.com/ysgeso/items/17c74178449148ad3ef7#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%83%8F%E3%83%B3%E3%83%89%E3%83%A9%E3%81%AE%E4%BB%AE%E5%BC%95%E6%95%B0%E3%81%AE%E5%80%A4%E3%81%AE%E6%84%8F%E5%91%B3)で説明したように __`on_play_changed` の仮引数__ に代入されたデータの __`"old"` 属性に変更前の値が記録されている__ ので、それを利用して下記のプログラムのように修正を行うことができます。

* __2 行目__：IntSlider の変更前の値は `changed.old` に代入されているので、それを `prev_frame` 属性に代入するようにする
* 4、7、11、14 行目の前にあったデバッグ用の `print` の表示はもう必要がないのですべて削除した

```python
 1  def create_event_handler(self):
 2      def on_play_changed(changed):
 3          self.prev_frame = changed.old
 4          self.update_gui()
元と同じなので省略
 5      def on_prev_button_clicked(b=None):
 6          self.play.value -= 1
 7          self.update_gui()
 8          
 9      def on_next_button_clicked(b=None):
10          self.play.value += 1
11          self.update_gui()
元と同じなので省略
12      def change_frame(edge_status, diff, status_list):
元と同じなので省略
13          self.play.value = frame
14          self.update_gui()
元と同じなので省略 
15  
16  Mbtree_Anim.create_event_handler = create_event_handler
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def create_event_handler(self):
    def on_play_changed(changed):
        self.prev_frame = changed.old
        self.update_gui()
            
    def on_prev_button_clicked(b=None):
        self.play.value -= 1
        self.update_gui()
        
    def on_next_button_clicked(b=None):
        self.play.value += 1
        self.update_gui()

    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)

    self.play.observe(on_play_changed, names="value")
    
    def change_frame(edge_status, diff, status_list):
        frame = self.play.value
        selectednode = self.mbtree.nodelist_by_score[frame]
        selectedstatus = self.mbtree.ablist_by_score[frame][3]
        if selectedstatus == edge_status:
            return
        while True:
            frame += diff
            node = self.mbtree.nodelist_by_score[frame]
            status = self.mbtree.ablist_by_score[frame][3]
            if node == selectednode and status in status_list:
                break
        self.play.value = frame
        self.update_gui()
            
    def on_node_first_button_clicked(b=None):
        change_frame("start", -1, ["start"])
            
    def on_node_prev_button_clicked(b=None):
        change_frame("start", -1, ["start", "score"])

    def on_node_next_button_clicked(b=None):
        change_frame("end", 1, ["end", "score"])
        
    def on_node_last_button_clicked(b=None):
        change_frame("end", 1, ["end"])
    
    if self.abfig is not None:
        self.node_first_button.on_click(on_node_first_button_clicked)
        self.node_prev_button.on_click(on_node_prev_button_clicked)
        self.node_next_button.on_click(on_node_next_button_clicked)
        self.node_last_button.on_click(on_node_last_button_clicked)
    
Mbtree_Anim.create_event_handler = create_event_handler
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def create_event_handler(self):
    def on_play_changed(changed):
+       self.prev_frame = changed.old
-       print("on_play_changed")
        self.update_gui()
元と同じなので省略
    def on_prev_button_clicked(b=None):
        self.play.value -= 1
-       print("on_prev_button_clicked")        
        self.update_gui()
        
    def on_next_button_clicked(b=None):
        self.play.value += 1
-       print("on_next_button_clicked")        
        self.update_gui()
元と同じなので省略
    def change_frame(edge_status, diff, status_list):
元と同じなので省略
        self.play.value = frame
-       print("change_frame")
        self.update_gui()
元と同じなので省略 

Mbtree_Anim.create_event_handler = create_event_handler
```
</details>

次に下記のプログラムのように __`update_ab`__ 内で __`prev_frame` 属性の値を更新する処理を削除__ する必要があります。

* 5 行目の後にあった `prev_frame` 属性を更新する処理と、`print` によるデバッグ表示を削除する

```python
 1  def update_ab(self):
元と同じなので省略
 2      for i in range(4):
 3          self.abax.text(15, 1 - i * 0.7, textlist[i])
 4          self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
 5          self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
 6  
 7  Mbtree_Anim.update_ab = update_ab
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def update_ab(self):
    alpha, beta, score, status, num_calculated, num_pruned = self.mbtree.ablist_by_score[self.play.value]
    maxnode = self.selectednode.mb.turn == Marubatsu.CIRCLE
    acolor = "red" if maxnode else "black"
    bcolor = "black" if maxnode else "red"
                            
    self.abax.clear()
    self.abax.set_xlim(-4, 23)
    self.abax.set_ylim(-1.5, 1.5)
    self.abax.axis("off")

    minus_inf = -3
    plus_inf = 4   
    alphacoord = max(minus_inf, alpha)
    betacoord = min(plus_inf, beta)
    
    color = "lightgray" if maxnode else "aqua"
    rect = patches.Rectangle(xy=(minus_inf, -0.5), width=alphacoord-minus_inf,
                            height=1, fc=color)
    self.abax.add_patch(rect)
    rect = patches.Rectangle(xy=(alphacoord, -0.5), width=betacoord-alphacoord,
                            height=1, fc="yellow")
    self.abax.add_patch(rect)
    color = "aqua" if maxnode else "lightgray"
    rect = patches.Rectangle(xy=(betacoord, -0.5), width=plus_inf-betacoord,
                            height=1, fc=color)
    self.abax.add_patch(rect)

    self.abax.plot(range(minus_inf, plus_inf + 1), [0] * (plus_inf + 1 - minus_inf) , "|-k")
    for num in range(minus_inf, plus_inf + 1):
        if num == minus_inf:
            numtext = "-∞"
        elif num == plus_inf:
            numtext = "∞"
        else:
            numtext = num
        self.abax.text(num, -1, numtext, ha="center")
        
    arrowprops = { "arrowstyle": "->"}
    self.abax.plot(alphacoord, 0, "or")
    self.abax.annotate(f"α = {alpha}", xy=(alphacoord, 0), xytext=(minus_inf, 1),
                    arrowprops=arrowprops, ha="center", c=acolor)
    self.abax.plot(betacoord, 0, "ob")
    self.abax.annotate(f"β = {beta}", xy=(betacoord, 0), xytext=(plus_inf, 1),
                    arrowprops=arrowprops, ha="center", c=bcolor)
    if score is not None:
        self.abax.plot(score, 0, "og")
        self.abax.annotate(f"score = {score}", xy=(score, 0),
                        xytext=((minus_inf + plus_inf) / 2, 1), 
                        arrowprops=arrowprops, ha="center")
            
    facecolorlist = ["aqua", "yellow", "lightgray"]
    textcolorlist = ["black", "black", "black"]
    if maxnode:
        nodetype = f"深さ {self.selectednode.mb.move_count} max node"
        textlist = ["β 狩り (β ≦ score)", "α 値の更新 (α < score < β)", "α 値の更新なし (score ≦ α)"]
        if score is not None :
            if beta <= score:
                textcolorlist[0] = "red"
            elif alpha < score:
                textcolorlist[1] = "red"
            else:
                textcolorlist[2] = "red"
    else:
        nodetype = f"深さ {self.selectednode.mb.move_count} min node"
        textlist = ["α 狩り (score <= α)", "β 値の更新 (α < score < β)", "β 値の更新なし (score ≦ β)"]
        if score is not None :
            if score <= alpha:
                textcolorlist[0] = "red"
            elif score < beta:
                textcolorlist[1] = "red"
            else:
                textcolorlist[2] = "red"
            
    if status == "start":
        facecolor = "white"
        nodetype += " 処理の開始"
    elif status == "score":
        facecolor = "lightyellow"
        nodetype += " 子ノードの評価値"
    elif status == "update":
        facecolor = "lightcyan"
        if maxnode:
            nodetype += " α 値の処理"
        else:
            nodetype += " β 値の処理"
    else:
        facecolor = "lavenderblush"
        nodetype += " 評価値の確定"
    self.abfig.set_facecolor(facecolor)
    self.abax.text(6, 1, nodetype)   
    for i in range(3):
        rect = patches.Rectangle(xy=(5, 0.3 - i * 0.7), width=0.8, height=0.5, fc=facecolorlist[i])
        self.abax.add_patch(rect)
        self.abax.text(6, 0.4 - i * 0.7, textlist[i], c=textcolorlist[i])    
    
    num_total = num_calculated + num_pruned
    num_ratio = num_calculated / num_total if num_total != 0 else 0
    _, _, _, _, prev_num_calculated, prev_num_pruned = self.mbtree.ablist_by_score[self.prev_frame]
    prev_num_total = prev_num_calculated + prev_num_pruned
    diff_num_calculated = num_calculated - prev_num_calculated
    diff_num_pruned = num_pruned - prev_num_pruned
    diff_num_total = num_total - prev_num_total
    diff_num_ratio = diff_num_calculated / diff_num_total if diff_num_total != 0 else 0

    textlist = [ "計算済", "枝狩り", "合計", "割合" ]
    datalist = [ num_calculated, num_pruned, num_total, f"{num_ratio * 100:.1f}%"]
    diff_datalist = [ f"{diff_num_calculated:+d}", f"{diff_num_pruned:+d}", 
                      f"{diff_num_total:+d}", f"{diff_num_ratio * 100:.1f}%"]
    for i in range(4):
        self.abax.text(15, 1 - i * 0.7, textlist[i])
        self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
        self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")

Mbtree_Anim.update_ab = update_ab
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def update_ab(self):
元と同じなので省略
    for i in range(4):
        self.abax.text(15, 1 - i * 0.7, textlist[i])
        self.abax.text(19.5, 1 - i * 0.7, datalist[i], ha="right")
        self.abax.text(22.5, 1 - i * 0.7, diff_datalist[i], ha="right")
-   self.prev_frame = self.play.value
-   print("update_ab")        

Mbtree_Anim.update_ab = update_ab
```
</details>

上記の修正後に下記のプログラムを実行し、選択中のノード内の移動の右の > ボタンをクリックすると実行結果のような図が表示されます。0 フレーム目からの差分として、その左の数値と同じ値が正しく表示されることが確認できます。

```python
Mbtree_Anim(mbtree, isscore=True)
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/6ad866de-01de-89e9-0e7c-122e599fe28f.png" width="500">

また、[今回の記事の最初](#mbtree_anim-のバグの修正と改良)で行ったように、4485 フレーム目から 4486 フレーム目に移動すると、下図のように右にフレーム間で行われた枝狩り数の __差分である 14694 - 9179 = 5515 が右の列に正しく表示__ されるようになったことが確認できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/1908b42b-952d-a707-25bc-1278684164a0.png" width="500">

# 今回の記事のまとめ

今回の記事では、Mbtre_Anim のバグの修正と、フレームを移動した際の A、P、M の __差分のデータを表示するという改良__ を行いました。

# 本記事で入力したプログラム

| リンク | 説明 |
|:--|:--|
| [marubatsu.ipynb](https://github.com/ysgeso/marubatsu/blob/master/148/marubatsu.ipynb)  | 本記事で入力して実行した JupyterLab のファイル |
| [tree.py](https://github.com/ysgeso/marubatsu/blob/master/148/tree_new.py)  | 本記事で更新した tree_new.py  |

# 次回の記事

https://qiita.com/ysgeso/items/ffa55bf637e6810b0330

[^1]:筆者も ipywidges の全ての機能を理解しているとは到底言えないと思います
[^2]:ウィジェットの操作の途中で継続的（continuos）に値を更新する（update）かどうかを指定するのでこのような名前になっています