Pythonで棋譜ファイル(.kif)から消費時間と評価値を抽出する方法

2020.2.13 グラフを一部修正

今回は備忘録的なもの。
消費時間をグラフ化したかったのでやってみた。

完成イメージ(消費時間:棒グラフ、評価値:折れ線グラフ)

画像23

使用ソフト・サイト
将棋倶楽部24・・・対局
ShogiGUI・・・棋譜解析
Jupyter Lab・・・データ抽出&グラフ作成
※環境によって.kifの仕様が変わると思われる

ShogiGUIで棋譜解析する。
棋譜コメントに読み筋を入れる(解析)にチェックを入れておく。

画像2

評価値の表示場所を確認する。
丁度一手だけ評価値がガクッと下がった局面があった。「-詰 13」となっていることがわかる。局面の評価値ではなく次の候補手の評価値。
他にも評価値が出ている(2604, 1443, 1156)が、それらはこの着手に替わる候補手の評価値である。

画像3

保存する。
着手と時間と評価値の場所を確認。一番上の「評価値 64」は初期配置の評価値、すぐ下の「評価値 44」が初手7六歩の評価値となっている。

画像4

Jupyter Labを起動。pandasのread_csvで読み込む。
encoding="Shift_JIS"を指定。
.txtに直さなくても.kifのまま読み込めた。

#棋譜ファイルを開く
import pandas as pd
df = pd.read_csv("20210206setsumeiyou1.kif", encoding="Shift_JIS")
df.head(20)

画像5

同じ行に手数、着手、評価値、、、と列を作ってくれない不親切設計。ヘッダー部を除外すると、行は順番に「着手→解析0→候補手→候補手→候補手」となっている。

必要な情報だけ抽出したい。
・手数
・消費時間
・評価値
「手数と消費時間」の行と「評価値」の行を取り出す。
最初の5行は不要なので読み込み時にスキップして読み込み直す。

# 最初の5行(タイトル行+0~3行)をスキップ
df = pd.read_csv("20210206setsumeiyou1.kif", encoding="Shift_JIS", skiprows=5)
#列名が長いので適当に短くする
df.rename(columns={"手数----指手---------消費時間--":"all"}, inplace=True)
df.head()

画像6

新しい列に該当行だけコピーする。

#着手の行は"*"で始まらない
df["kifu"] = df[df["all"].str[:1]!="*"]["all"]
#評価値の行は最初の6文字が"**解析 0"で始まる
df["value"] = df[df["all"].str[:6]=="**解析 0"]["all"]
df.head()

画像7

長すぎて表示しきれない部分を見たい

df["all"][3]

画像8

評価値は「評価値」と「読み筋」の間に位置する。

#文字列を"評価値""読み筋"で区切ってみる
df["value"].str.split("評価値|読み筋", expand=True).head(10)

画像9

「列1」に評価値の数値らしきものが抽出できた。
dfの「value」列を置き換える。

#value列を「数値らしきもの」だけに置きかえる
df["value"] = df["value"].str.split("評価値|読み筋", expand=True)[1]
#ついでにall列を削除
del df["all"]
df.head(10)

画像10

#value列を1行ずらす
df["value"] = df["value"].shift(-1)
#kifu列にもvalue列にもデータのない行を削除
df = df.dropna(subset=["value", "kifu"], how="all")
#行番号リセット
df.reset_index(drop=True, inplace=True)
df.head()

画像11

#末尾はどうなっているか
df.tail()

画像12

0行目と最終行を消去。

#先頭行消去
df = df[1:]
#最終行消去
df = df[:-1]
df

画像13

次に手数と消費時間を抽出する。
表示は 「手数 指し手 (消費時間/累計消費時間)」となっている。

kifu列の最初の数字が手数だが、実際は「□□□□1」のようにスペースが並んでいて5文字分ある。

#手数: kifu列の左から5文字分
df["move"] = df["kifu"].str[:5]
df.head()

画像14

消費時間を抽出する。kifu列を「/」で区切ってみる。

#"/"で区切る
df["kifu"].str.split("/", expand=True)

画像15

0列の末尾4文字が消費時間になっている。

#列0の方の末尾4文字を抽出
tmp = df["kifu"].str.split("/", expand=True)[0].str[-4:]
tmp

画像16

この表示だと9:59秒を超えたらどうなるのか気になるが、レアケースなので考慮しない。扱いやすいように秒に換算する。

#分:前1文字, 秒:末尾2文字
minute = tmp.str[:1].astype(int)
second = tmp.str[-2:].astype(int)
df["time"] = minute * 60 + second
df.head()

画像17

手番の列も作っておく。

#手番: 手数が奇数: 先手, 手数が偶数: 後手
#とりあえず全部「後手」を入力して、奇数だけ「先手」に上書きする
df["turn"] = "後手"
df["turn"][::2] = "先手"
df.head()

画像18

だいたい欲しい情報が揃ってきた。型をint型に変更したいが、その前にやることがある。

#評価値をよくみると・・・
df["value"].unique()

画像19

「詰」を数値に変える。
評価値に矢印が付いているときがあるので削除する。
投了時が「nan」になっているので

# +詰, -詰 を99999, -99999に置換
df["value"].mask(df["value"].str[:3]==" +詰", "99999", inplace=True)
df["value"].mask(df["value"].str[:3]==" -詰", "-99999", inplace=True)
#スペース消去, 矢印消去
df["value"] = df["value"].str.replace(" ", "")
df["value"] = df["value"].str.replace("↑", "")
df["value"] = df["value"].str.replace("↓", "")
#最終行のvalueを最後から2番目の行と同じ値にする
df["value"][len(df)] = df["value"][len(df)-1]

型をint型に変更する。

#型をint型に変更
df["value"] = df["value"].astype(int)
df["move"] = df["move"].astype(int)
#必要な部分だけ取り出し, 並び替え
df = df[["move", "turn", "time", "value"]]
df

画像20

グラフを作るようにもう少し前処理する。棒グラフの上を先手、下を後手にするため、後手の消費時間の符号をマイナスにする。

#後手番の時間をマイナスにする
df["time_rev"] = df["time"]
df["time_rev"][1::2] = df["time_rev"][1::2] * -1

評価値を上限を2000にする。

#2000以上を2000, -2000以下を-2000にする
df["value_lim"] = df["value"]
df.loc[df["value_lim"]<-2000, "value_lim"] = -2000
df.loc[df["value_lim"]>2000, "value_lim"] = 2000
df

画像21

これで前処理完了。
あとはグラフにするだけ。
matplotlibとかをインポートする。
好みのスタイルとカラーパレットを使う。

import matplotlib.pyplot as plt
import japanize_matplotlib
import seaborn as sns
%matplotlib inline
sns.set_style("white")
sns.set_palette("pastel")

グラフの書き方は色々ある。

fig, ax = plt.subplots(figsize=(10,4), tight_layout=True)
ax.grid()

ax.plot(df["move"], df["value_lim"], color="gray", alpha=0.7)
ax.set_ylim([-2010, 2010])
ax.set_ylabel("評価値(折れ線グラフ)")

ax2 = ax.twinx()
ax2.hlines(0, 0, max(df["move"]), color="gray", alpha=0.2)
ax2.bar(df.loc[df["turn"]=="先手", "move"], df.loc[df["turn"]=="先手", "time_rev"], label="先手")
ax2.bar(df.loc[df["turn"]=="後手", "move"], df.loc[df["turn"]=="後手", "time_rev"], label="後手")
ax2.set_xlim(0, max(df["move"])+1)
ax2.set_xticks(range(0, max(df["move"]), 10))
ax2.set_xticklabels(range(0, max(df["move"]), 10))
ax2.set_xlabel("手数")
ax2.set_ylim([max(df["time"])*-1, max(df["time"])])
ax2.set_ylabel("消費時間(棒グラフ)")
ax2.legend(loc="upper left")

fig.patch.set_facecolor("white")
fig.savefig("time_and_value_1.png")

画像23

seabornでも書ける。

fig, ax = plt.subplots(tight_layout=True, figsize=(10, 5))
ax.grid()
ax2 = ax.twinx()

sns.lineplot(data=df, x="move", y="value_lim", ax=ax, color="gray", alpha=0.5)
ax.set_ylim(-2000, 2000)
ax.set_ylabel("評価値(折れ線グラフ)")

sns.barplot(data=df, x="move", y="time_rev", hue="turn", ax=ax2)
ax2.set_xticks(range(0, max(df["move"]), 10))
ax2.set_xticklabels(range(0, max(df["move"]), 10))
ax2.set_ylabel("消費時間(棒グラフ)")
ax2.set_ylim(max(df["time"])*-1, max(df["time"]))
ax2.legend(loc="upper left")

fig.patch.set_facecolor("white")
fig.savefig("time_and_value_2.png")

画像23

今回はここまで。

いいなと思ったら応援しよう!