(2019-11-26 ガブリアスのげきりんの威力が間違えていることに気が付いたので、一部修正)
利用者スタッフのrickyだ。
ネットショップ運営の業務の中で、前からCSVデータの操作法については、要望の声が多かったように思う。
CSVファイルは、一応Excelでも読み込むことが出来るし、
基本的な操作なら、それで事足りるかもしれない。
が、かれこれExcelというものはいちいちおせっかいなものだ。
読み込むのに、ヘッダーを選択する必要がある。
望まない数列を小数点に置き換えて、元に戻さない。
Janコードや郵便番号といった膨大な数を指数表記にしてしまう。
そもそもエンコーディングが一致していなくて、読み込めない。(読み込めても、文字化けしている)
イライラポイントを挙げていれば、枚挙にいとまがないのだ。(若干自分も、書いていていらついてきている。)
そこで、別の操作方法として、python3とpandasとJupyter-notebookを用いて、CSVを読み込み、操作する方法までを紹介していきたい。
jupyter-notebookは、事業所内の仮想環境の中に、誰でもアクセス可能な形で置いている。
不明な点があれば、問い合わせてくれ。
さて、いきなり無味乾燥なネットショップの商材データに利用者を向い合せるのも些か酷な話でもあるので、
この記事に興味を持つであろう世代に最も親しみやすいと思われる例として
ポケモンのCSVデータの解析、及びダメージ計算、
これを基として説明していきたいと思う。
ポケモンのCSVデータは kaggle という
機械学習、及びデータ分析のためのサイトが
配布しているものを用意した。
https://www.kaggle.com/
(※要アカウント登録)
上記のトピックスからダウンロード。
CSVファイル自体は仮想環境内に、誰でもアクセス可能な状態で置いてある。
各々がダウンロードする必要はない。
このCSVデータを用いて、自分がどういう操作をしたのか紹介していこう。
入力は In:[n] 、それに対する出力を Out:[n]として記述している。
詳しくは、Jupyter-Notebookの /pokemon/pokemon.ipynb に入っているのを見てほしい。(恐らくは、そっちの方が分かりやすいはずだ。)
In[1] :
import pandas as pd
#行列、表を扱うためのプログラムの集合
#読み込み時に、pdという名前を付けている。
pandas とは、CSVをデータフレームという形に変更して、pythonの中で扱いやすくするモノである。(こういったものを、ライブラリやモジュールと呼ぶ。) これらの基本的な使い方は、仮想環境内の過去記事に置いてある。
In[2] :
dt = pd.read_csv("pokemon.csv")
#pandasの中のcsvを読み込むための関数を使用。
#括弧の中の文字は、読み込むcsvの名前を入れる。
Out[2] :
![](https://stat.ameba.jp/user_images/20191125/14/work-plus/2a/f0/j/o0640018514649280442.jpg?caw=800)
CSVは、一行目がヘッダーであることが多い。
Out[2] : の一番上の行、そのポケモンの個性を説明するためのセルが、この場合のヘッダーに当たる。
しかしながら、Jupyter-notebookでは、pandasのデータの表示は折りたたまれており、
非常に見にくいのだ。(一応、全部の列を表示する設定もあるが、スクロールバーが追加されて重くなり、 余計に不便になるだけなのだ。)
ここでCSVがどんな項目を持っているのかを見てやる。
In[3] :
dt.columns
#CSVがどんなヘッダーがあるかを調べる
Out[3] :
Index(['abilities', 'against_bug', 'against_dark', 'against_dragon',
'against_electric', 'against_fairy', 'against_fight', 'against_fire',
'against_flying', 'against_ghost', 'against_grass', 'against_ground',
'against_ice', 'against_normal', 'against_poison', 'against_psychic',
'against_rock', 'against_steel', 'against_water', 'attack',
'base_egg_steps', 'base_happiness', 'base_total', 'capture_rate',
'classfication', 'defense', 'experience_growth', 'height_m', 'hp',
'japanese_name', 'name', 'percentage_male', 'pokedex_number',
'sp_attack', 'sp_defense', 'speed', 'type1', 'type2', 'weight_kg',
'generation', 'is_legendary'],
dtype='object')
Out[3] を見るに、これだと表が見切れてしまうので、
ダメージの計算に必要な本質的なカラム、もしくはこれから
必要になるカラムを選定していこうと思う。
pokedex_number,name,japanese_name は、それぞれ図鑑番号,英名,和名である(ポケモンの識別に必要)
type1,type2 は、そのポケモンのタイプである。(ダメージの計算に必要)
hp,attack,defense,sp_attack,sp_defense,speed はそれぞれH,A,D,SA,SD,Sである。(ステータス計算に必要)
against_*** は、そのタイプの攻撃を食らった場合、ダメージにどれだけの補正がかかるのかを示す。(ダメージ計算に必要そうに見えるが、こんなものをフレームに含んでいるときりがないので除外。)
is_legendary は、そのポケモンが伝説のポケモンであるかどうかを示す。(今回は必要ないが、この先使うかもしれないので加入)
generation は、そのポケモンが何世代目のポケモンであるかを示す(今回は必要ないが、この先使うかもしれないので加入)
今回は以上のカラムを、データ表示に使っている。
In[4] :
dt = dt[['pokedex_number','name','japanese_name','type1', 'type2', \
'hp','attack','defense','sp_attack', 'sp_defense', 'speed', \
'is_legendary','generation']]
dt
#csvのカラムの中から、必要な情報だけを取り出す。
# 図鑑番号、英名、和名、タイプ1、タイプ2、
#HP,こうげき、ぼうぎょ、とくこう、とくぼう、すばやさ、伝説ポケモンかどうか、バージョン
Out[4] :
![](https://stat.ameba.jp/user_images/20191125/14/work-plus/fa/12/j/o0640021014649284404.jpg?caw=800)
さて、ここで一問、例題を挙げてみたいと思う。
(問1) レベル50(全個体値0,全努力値0,性格補正なし)のピカチュウが、 レベル50(全個体値0,全努力値0,性格補正なし) のニャースに10万ボルト(威力 : 90, タイプ : でんき)を使った場合、 与えられると予想されるダメージと、ニャースの残りHPを計算せよ。
ピカチュウが、ニャースに10万ボルトを放って、ロケット団の2人ごと吹っ飛ばされていく有様は、 昔のポケモンアニメでよく見られた光景だった。 では、この現象を実際に計算したら、どうなのだろうか?
In[5] :
#csvの中から、ピカチュウを探す。
pikachu = dt[ dt["japanese_name"].str.contains("ピカチュウ") ]
#テーブルの中の、和名のカラムから、"ピカチュウ"という文字列を含むインデックスを検索する。
#カラム.str.contains(…) とは、指定のカラムの中から、中の文字列を含むインデックスを返す関数である。
pikachu
Out[5] :
pokedex_number | name | japanese_name | type1 | type2 | hp | attack | defense | sp_attack | sp_defense | speed | is_legendary | generation | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
24 | 25 | Pikachu | Pikachuピカチュウ | electric | NaN | 35 | 55 | 40 | 50 | 50 | 90 | 0 | 1 |
In[6] :
#ピカチュウのとくこう("sp_attack")を表示。
pika_sp_attack = int(pikachu["sp_attack"])
ピカチュウのとくこうの種族値は %s です" % pika_sp_attack
Out[6] :
'ピカチュウのとくこうの種族値は 50 です'
In[7] :
#csvの中から、ニャースを検索する。
nyasu = dt[dt["japanese_name"].str.contains("ニャース")]
#テーブルの中の、和名のカラムから、"ニャース"という文字列を含むインデックスを検索する。
nyasu
Out[7] :
pokedex_number | name | japanese_name | type1 | type2 | hp | attack | defense | sp_attack | sp_defense | speed | is_legendary | generation | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
51 | 52 | Meowth | Nyarthニャース | normal | dark | 40 | 35 | 35 | 50 | 40 | 90 | 0 | 1 |
In[8] :
nyasu_hp , nyasu_sp_defense = int(nyasu["hp"]), int(nyasu["sp_defense"])
"ニャースのHPの種族値は %s , とくぼうの種族値は %s です。" % (nyasu_hp,nyasu_sp_defense)
Out[8] :
'ニャースのHPの種族値は 40 , とくぼうの種族値は 40 です。'
In[9] :
#さてここで、レベル50,全個体値0, 全努力値0、性格補正 1 のピカチュウが
#レベル50,全個体値0, 全努力値0, 性格補正 1 のニャースに、10万ボルト(威力 : 90, でんき)を打った場合の
#ダメージの実数値を求めたいと思う。
#ポケモンの種族値からステータス実数値を求める式は
#HP の場合 : (種族値×2+個体値+努力値÷4)×レベル÷100+レベル+10
#それ以外の能力 : {(種族値×2+個体値+努力値÷4)×レベル÷100+5}×せいかく補正
#で求められる。
In[10] :
#ピカチュウのとくこうの実数値を求める。
level = 50 #レベル は 50
individual_val = 0 #個体値 は 0
effort_val = 0 #努力値 は 0
nature = 1 #性格補正 は 1
SA = int(pika_sp_attack * 2) #種族値に2をかけて切り捨て
SA = int(SA + individual_val + int(effort_val / 4)) #個体値と努力値を4で割った結果を足し
SA = int(SA * level / 100) + 5 #レベルでかけてから100で割った値に5を足し
SA = int(SA * nature) #性格の補正値を掛ける
"ピカチュウ( LV50, 全個体値 0 , 全努力値 0, 性格補正 1 )のとくこうの実数値は %s です" % SA
Out[10] :
'ピカチュウ( LV50, 全個体値 0 , 全努力値 0, 性格補正 1 )のとくこうの実数値は 55 です'
In[11] :
#哀れにも攻撃を受けることになるロケット団のニャースのHPと、とくぼうの実数値を求める。
level = 50
individual_val = 0
effort_val = 0
nature = 1
HP = int(int(int(nyasu_hp * 2) + individual_val + int(effort_val / 4) ) * level / 100 ) + level + 10
SD = int(int(int(nyasu_sp_defense * 2) + individual_val + int(effort_val / 4) ) * level / 100 + 5 ) * nature
"ニャース( LV50, 個体値 0 , 努力値 0 )のHPの実数値は %s 、とくぼうの実数値は、%s です。" % (HP,SD)
Out[11] :
'ニャース( LV50, 個体値 0 , 努力値 0 )のHPの実数値は 100 、とくぼうの実数値は、45 です。'
In[12] :
#ダメージの計算は、
#ダメージ = int(int(int(攻撃者のレベル×2/5+2)×威力×A/D)/50+2)×各種ダメージ補正
#で求められる
level = 50
damage = int(level * 2 / 5 + 2)
skill_power = 90
damage = int( damage * skill_power * SA / SD )
damage = damage / 50 + 2
damage = int(damage * 1.5)
"ピカチュウの10万ボルトによる、ニャースへのダメージは %s です。(残りHP %s )" % (damage, HP - damage)
Out[12] :
'ピカチュウの10万ボルトによる、ニャースへのダメージは 75 です。(残りHP 25 )'
In[13] :
#なお、実際のポケモンのダメージには、ダメージの計算に 0.85 ~ 1 までの乱数がかかっており、
#実際には、上のダメージ計算結果を下回ることの方が多い。
"ピカチュウの10万ボルトによる、ニャースへの取りうるダメージの範囲は %s ~ %s です。" % (int(damage * 0.85),damage)
Out[13] :
'ピカチュウの10万ボルトによる、ニャースへの取りうるダメージの範囲は 63 ~ 75 です。'
以上が、計算の答えだ。 いかにも一撃でやられてそうに見えるが、もしピカチュウがニャースと同等の力を持っていた場合 ピカチュウの10万ボルトをもってさえも確定一発に持ち込めないのである。 ということは、実際には個体値、努力値、あるいはレベル差があるに違いない。
ちなみに、上記の種族値 -> 実数値 , 実数値 -> 実際のダメージ量 の計算の手順は、 関数化すると非常に扱いやすくなる。
In[14] :
#なお、上記の、計算であるが pythonをはじめとするプログラミング言語は、手順を関数化することにより
#コーディングにかかる手間と記述量を圧倒的に減らすことが出来る。
#その記法は
#def {関数名}(引数1,引数2,…,引数n,):
#____計算1
#____計算2
#____(…)
#____計算n
#____return <計算結果>
#だ。
#ただし、上記のアンダーバーは、 半 角 ス ペ ー ス に置き換えてもらいたい。
#というのも、pythonでは、関数の内容や、スコープを半角スペース4つ記載するのがルールとなっており、
#これを無視すると、基本的に実行すらできないからだ。
#これをあえてアンダーバーにしてある理由は、もはや説明するまでもないだろう。
In[15] :
#種族値、レベル、個体値、努力値、性格から、ステータス実数値を求める与式を関数化(HP以外)
def from_base_to_stats(base_stats, level, individual_val, effort_val ,nature = 1):
result = int(base_stats * 2)
result = int(result + individual_val + int(effort_val / 4))
result = int(result * level / 100) + 5
result = int(result * nature)
return result
In[16] :
#種族値、レベル、個体値、努力値から、HPの実数値を求める与式を関数化
def from_base_hp_to_stats_hp(base_stats, level, individual_val, effort_val):
result = int(base_stats * 2)
result = int(result + individual_val + int(effort_val / 4))
result = int(result * level / 100)
result = result + level + 10
return result
In[17] :
#攻撃ポケモンのこうげき(とくこう)、攻撃ポケモンのレベル、攻撃されるポケモンのぼうぎょ(とくぼう)、わざの威力、その他の補正から
#ダメージを求める式を関数化
def require_damage(A, D, attacker_level, skill_power, other_effect = 1):
result = int(attacker_level * 2 / 5 + 2)
result = int(result * skill_power * A / D)
result = result / 50 + 2
result = int(result * other_effect)
min_value = int(result * 0.85)
return result,min_value
上記のように、手順を関数化しておくと {関数の名前}(引数1,引数2, ... 引数n) で簡単に答えが出せるようになる。
では、もう少し実践的な1問を追加してみよう。 (問2) レベル50(全個体値31,こうげき努力値252,いじっぱり)のガブリアスが、 レベル50(全個体値31,HPの努力値0,ぼうぎょ努力値MAX(252),きまぐれのスイクンに1ガブリアスのげきりん(威力:100,タイプ:ドラゴン)を使った場合、 与えられると予想されるダメージと、スイクンの残りHPを計算せよ。
In[18] :
#それでは、
#レベル50,個体値6V(31),こうげきの努力値MAX(252),いじっぱり(性格補正 : 1.1)の、ガブリアスのげきりん(威力:120,タイプ:ドラゴン)
#を
#レベル50,個体値6V(31),HPの努力値0,ぼうぎょ努力値MAX(252),きまぐれ(性格補正 : 1 ) の スイクン
#が受ける場合のダメージと、残りHPを求めるにはどうすればいいか?
In[19] :
#csvのデータテーブルから、ガブリアスを検索したはいいが、
#どう見ても、通常のガブリアスの能力値ではなく、メガガブリアスの能力値となっている。
#このように、kaggleで配布されたデータだからといって、必ずしも間違いがないとは限らない。
#見てもらえばわかると思うが、一部ポケモンの高さや重さにも欠損値が含まれている。(実はこれが、プロジェクトの狙いかもしれないが)。
dt[dt["japanese_name"].str.contains("ガブリアス")]
Out[19] :
pokedex_number | name | japanese_name | type1 | type2 | hp | attack | defense | sp_attack | sp_defense | speed | is_legendary | generation | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
444 | 445 | Garchomp | Gaburiasガブリアス | dragon | ground | 108 | 170 | 115 | 120 | 95 | 92 | 0 | 4 |
In[20] :
#実際のガブリアスのこうげき種族値は、130であることが知られている。
gabu_A = from_base_to_stats(130,50,31,252,1.1)
"ガブリアスのこうげきの実数値は %s です。" % gabu_A
Out[20] :
'ガブリアスのこうげきの実数値は 200 です。'
In[21] :
suikun = dt[dt["japanese_name"].str.contains("スイクン")]
suikun
Out[21] :
pokedex_number | name | japanese_name | type1 | type2 | hp | attack | defense | sp_attack | sp_defense | speed | is_legendary | generation | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
244 | 245 | Suicune | Suicuneスイクン | water | NaN | 100 | 75 | 115 | 90 | 115 | 85 | 1 | 2 |
In[22] :
#スイクンのHPと、ぼうぎょの実数値を求める。
suikun_H,suikun_D = int(suikun["hp"]),int(suikun["defense"])
suikun_H,suikun_D = from_base_hp_to_stats_hp(suikun_H,50,31,0), \
from_base_to_stats(suikun_D,50,31,252)
"スイクンのHP,ぼうぎょ の 実数値は、それぞれ %s %s です。" % (suikun_H,suikun_D)
Out[22] :
'スイクンのHP,ぼうぎょ の 実数値は、それぞれ 175 167 です。'
In[23] :
#ステータスとレベル、わざの威力から
#ダメージの実数値を求める。
#ちなみに、げきりんはタイプ一致なので、ピカチュウの10万ボルトと同じく1.5の補正がかかる。
result = require_damage(gabu_A,suikun_D,50,120,other_effect = 1.5)
"スイクンに対する、ガブリアスのげきりんが取りうるダメージの範囲は、%s ~ %s です。(残りHP:%s ~ %s)" % \
(result[1],result[0],suikun_H - result[1],suikun_H - result[0])
Out[23] :
'スイクンに対する、ガブリアスのげきりんが取りうるダメージの範囲は、82 ~ 97 です。(残りHP:93 ~ 78)'
以上の通りである。 スイクンはガブリアスの4世代当時最高峰のステータスから放たれる、げきりんを少なくとも1発は耐えることができ、 更に大抵の場合、れいとうビームを持っていたので当時のトップメタとして、挙げられるポケモンの一体だった。
(問3) レベル100,6V,せっかちのミュウと、 レベル100,6V,性格補正なし,素早さの努力値無振のミュウツーが戦闘した場合、必ずミュウが先手をとりたい場合、 振るべき努力値の最小値を求めよ。
素早さのステータスは、ダメージの計算とは少し勝手が違い、 その実ステータスが1でも高い方が必ず先手を取る(乱数要素が一切ない)。 ポケモンでの素早さは重要なステータスだというが、環境ポケモンが固定されている以上、 同じポケモンのミラー戦になることは非常に多かったのだ。 素早さが1でも多く上回るということは、下回ってしまった方が先にやられる可能性が高いということを意味している。
In[24] :
#では、6V性格せっかちのミュウと
#6V,性格補正なし,素早さ努力値無振のミュウツーが戦闘した場合
#ミュウがミュウツーよりも先手を取れるようにするには、どれだけの努力値を振ればよいか?
In[25] :
#csvの中から、ミュウを探す
dt[dt["japanese_name"].str.contains("ミュウ")]
#なんと、和名がミュウのテーブルを調べたいのに、
#同時にミュウツーのデータまでついてきてしまった。
# というのも、この{pd.DataFrame}.str.contains という関数は、中の文字列を 含むもの 全てを検索してしまう。
# 検索文字列が "ミュウツー" であれば、その結果は絶対にミュウツーのもののみだと言い切れるが、
# 検索文字列が "ミュウ" であれば、その結果は必ずしもミュウのものだけとは限らない。
Out[25] :
pokedex_number | name | japanese_name | type1 | type2 | hp | attack | defense | sp_attack | sp_defense | speed | is_legendary | generation | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
149 | 150 | Mewtwo | Mewtwoミュウツー | psychic | NaN | 106 | 150 | 70 | 194 | 120 | 140 | 1 | 1 |
150 | 151 | Mew | Mewミュウ | psychic | NaN | 100 | 100 | 100 | 100 | 100 | 100 | 1 | 1 |
In[26] :
#しかし、上記結果から、ミュウの図鑑番号(pokedex_number)が151であるという重要な手がかりは得られた。
mew = dt[dt["pokedex_number"] == 151]
mew_speed_base = int(mew["speed"])
"ミュウのすばやさの種族値は %s です。" % mew_speed_base
Out[26] :
'ミュウのすばやさの種族値は 100 です。'
In[27] :
#ミュウツーの素早さであるが、例によってメガシンカした時の素早さが入ってしまっている。
#本来のミュウツーの素早さの種族値はは130であるので、それで計算した
mewtwo_speed_base = 130
"ミュウツーのすばやさの種族値は %s です。" % mewtwo_speed_base
Out[27] :
'ミュウツーのすばやさの種族値は 130 です。'
In[28] :
#その前に、能力値の計算に、努力値がどこまで影響を及ぼしてるのかのケースを見てみる。
#ミュウツーの努力値無振と、努力値全振を比較した場合どうか?
mewtwo_speed_6V0E = from_base_to_stats(mewtwo_speed_base,100,31,0,1)
mewtwo_speed_6V252E = from_base_to_stats(mewtwo_speed_base,100,31,252,1)
"ミュウツー(6V性格補正なし)の、すばやさの実数値は %s(努力値無振り) %s(努力値MAX) です。" % (mewtwo_speed_6V0E ,mewtwo_speed_6V252E)
Out[28] :
'ミュウツー(6V性格補正なし)の、すばやさの実数値は 296(努力値無振り) 359(努力値MAX) です。'
In[29] :
#同様の計算をミュウで行った場合はどうか?
mew_speed_6V0E = from_base_to_stats(mew_speed_base,100,31,0,1.1)
mew_speed_6V252E = from_base_to_stats(mew_speed_base,100,31,252,1.1)
"ミュウ(6V性格補正あり)の、すばやさの実数値は %s(努力値無振り) %s(努力値MAX) です。" % (mew_speed_6V0E ,mew_speed_6V252E)
Out[29] :
'ミュウ(6V性格補正あり)の、すばやさの実数値は 259(努力値無振り) 328(努力値MAX) です。'
In[30] :
i = 0
val = from_base_to_stats(mewtwo_speed_base,100,31,0,1)
while (i < 253):
result = from_base_to_stats(mew_speed_base,100,31,i,1.1)
if (val < result):
break
else:
i = i + 1
continue
"ミュウツーに対して先手を取りたい場合、ミュウに振るべき努力値は %s です" % i
Out[30] :
'ミュウツーに対して先手を取りたい場合、ミュウに振るべき努力値は 136 です'
ということだ。昔のポケモン対戦ではミュウとミュウツーが戦ったら、その殆どがミュウツーが先手をとっていたことだろう。 しかし、もしミュウに努力値を振った最速のケースを考えてみた場合、 才能も努力も積んだミュウが、才能はあるが努力をしていないミュウツーを上回るといった事態も十分ありえるという結果になった。 もっとも言い方を変えれば、 才能のあるものが更に努力をした場合、 その才能を下回るものがどれだけ努力を積んだところで絶対に及ばないという 皮肉な結果も同時に意味することとなるが。
以上、簡潔なポケモンのCSVの操作を自分なりに行ってみたものだ。
本記事は事業所の利用者の理解を深めるために所内の仮想環境内で書いたものだ。 正直言って、Amebaのブログにこれを転載するつもりなど、これっぽっちもなかった。
それでも、俺がこの記事をAmebaに乗せようと思ったのは、 編集者はポケモンを第1-5世代までやっていて、ある程度対人戦もこなしていたが、 正直、ここまで厳密な数字を自分の手で求めることを、意識したことがない。 つまるところ、上記の自分の計算が合っている保証など、どこにもないのだ。
仮に、上記の計算に間違いが無かったとしても、参考資料を見る内に世代によって、 一部計算の順序や倍率が微妙に異なっているなど、まま有りうる。
もし、計算ミスや世代間のギャップを指摘してくれそうな"誰か"の目に留まればいいが、 それが無いと、間違った計算が、あたかも本当のことのように知れ渡ってしまうのだ。
計算が間違っている?もっといい計算方法がある?
至急、もっといい計算式くれや