Tidy Data(整然データ)って何?Pandasのmelt()メソッドで実現できるらしいけど...

f:id:ts0818:20210331195319j:plain

ベーラー(Baler)は農業機械の一種で、刈取って寄せ集めた干し草のような作物を圧縮して梱包し、トワイン(twine)で結束するために使う梱包機械である。

ベーラー - Wikipedia

最もよく使われるタイプのベーラーは、円柱状に成型された梱包を作るロールベーラーである。干し草はベーラーの内部で、ゴムベルトかローラー、またはそれらを組み合せた機構によって単純に巻き上げられる。

ベーラー - Wikipedia

酪農等の200頭以上の牛を飼う現代の大規模農業において、1トンかそれ以上のロールベールは検討に値する。しかしながら、ロールベールは斜面を転がったりするため、特別な運搬方法や荷役機械を必要とする。

ベーラー - Wikipedia

⇧ 映画とかでしか見たこと無いけど「干し草」を丸めたやつって「ロールベール」ってのが正規名称だったんですな、それにしても重量が「1トン」超えるの!

衝撃が冷めやらず、ザワザワしちゃってます、どうもボクです。

By the way、「教師なし機械学習」の1つ「K-means」で、「機械学習モデルの性能評価」のためには「正解データ」が必要なことに、最近気づいた情弱な私です...。

www.scutum.jp

そして、遂に2020年のGW、冒頭で書いた2冊の本を同時並行的に読んでいる中で、答えにたどり着くことができました。「Outlier Analysis」のP.30の「1.7.2 Common Mistakes in Benchmarking」に答えが書いてあります。より正確にはこのブログだけでなく原書をぜひ読んでみてください(※1)。

要するに、答えは

「できない」

でした(汗。

教師なし学習の性能評価は雰囲気でやるしかない - WAF Tech Blog | クラウド型 WAFサービス Scutum 【スキュータム】

⇧ 上記サイト様の記事で、確信に変わった次第です。

そうなんですよ、確かに「教師なし機械学習」で作成する「機械学習モデル」の「学習」には「答え」が必要ないんですけど、「性能評価」の時に「答え」が必要になってくるってのがね、盲点でしたですね。

そんなわけで、「モデルの性能評価」はできてませんが、

github.com

⇧ 「教師なし機械学習」の1つ「K-means」を実施してみました。Google Colaboratryで動くかと~。

ただ、「K-means」使った意味が無かったので、「K-means」が活かせる方法を模索していきたいところです。

2021年4月13日(火)追記:↓ ここから

なんか、

qiita.com

k近傍法(k-nearest neighbor :k-NN)とk平均法(k-means clustering)というのは名前も似てて最初は同じものだと誤解する人もいるでしょう.現に私もそう思っていました.実際には似たようなアルゴリズムの考え方ですが,若干その考え方が違います.

k近傍法とk平均法の違いと詳細. - Qiita

⇧ ってことみたいなので、要注意ですね。自分も違い分かってなかったっす...

2021年4月13日(火)追記:↑ ここまで

そんでは、今回は、「Tidy Data(整然データ)」について調べてみました。

レッツトライ~。

 

Tidy Data(整然データ)って何?

Pythonのライブラリである「Pandas」の公式のCheet Sheetで、

⇧「Tidy Data」って言葉が出てくるのですが、What’s Tidy Data?って思ったので調べてみました。

qiita.com

皆さん「Tidy Data 」(日本語では「整然データ」と訳されています。) というコトバを聞いたことがありますでしょうか?

整然データ(Tidy Data)への変換をpandasでやってみる - Qiita

Tidy Dataとは、要は「分析者の仕事の80%を占める」と言われるデータのクリーニング/整理作業をできるだけ簡単で効果的に行うための概念および手法です。

整然データ(Tidy Data)への変換をpandasでやってみる - Qiita

⇧ 上記サイト様が分かりやすいです。

上記サイト様で紹介のあったサイトに「整然データ」の説明があります。

id.fnshr.info

概要

整然データとは、1) 個々の変数が1つの列をなす、2) 個々の観測が1つの行をなす、3) 個々の観測の構成単位の類型が1つの表をなす、4) 個々の値が1つのセルをなす、という4つの条件を満たした表型のデータのことであり、構造と意味が合致するという特徴を持つ。R言語などを用いたデータ分析の際には非常に有用な概念である。

整然データとは何か|Colorless Green Ideas

⇧ ってな感じで、

  1. 個々の変数が1つの列をなす
  2. 個々の観測が1つの行をなす
  3. 個々の観測の構成単位の類型が1つの表をなす
  4. 個々の値が1つのセルをなす

の4つの条件を満たす表型のデータを「Tidy Data(整然データ)」 というらしい。

Wikipediaができてたみたい。

整然データ (せいぜんデータ、: Tidy data) は、 次の4つの条件を満たした表型データのこと。

  1. 個々の変数が1つの列をなす
  2. 個々の観測が1つの行をなす
  3. 個々の観測の構成単位の類型が1つの表をなす
  4. 個々の値が1つのセルをなす

構造と意味が合致するという特徴を持ち、R言語などを用いたデータ分析の際には非常に有用な概念である

Tidy data - Wikipedia

 

「Pandas」の「melt()」メソッドで「Tidy Data(整然データ)」にできるらしい

データ分析するなら「整然データ」が良いらしいということで、Pythonを使ってる場合は「Pandas」に用意されてる「melt()」 メソッドを使うと「Tidy Data(整然データ)」に変換できるようです。

⇧「Pandas」公式のCheet Sheetの「Reshaping Data - Change the layout of a data set」の中で紹介されていますね。

「Pandas」の「melt()」 メソッドについての「API Refernce」は以下ですね。

pandas.pydata.org

メソッド実効後のイメージ図は以下ですね。

pandas.pydata.org

⇧「melt()」 メソッドで、どうして、こういう形に変形するのかってのがいまいち腑に落ちてなかったのですが、「Tidy Data(整然データ)」っていう考え方を前提にしてたってことなんですかね。

 

「カテゴリ変数(質的データ)」を「ダミー変数化(one-hot表現)」後に「Tidy Data(整然データ)」にする?

「Tidy Data(整然データ)」 にするべきってことは分かったのですが、ここで今一度、データサイエンスで扱う「データ」がどうなっているのが望ましいのかの原点に戻ってみますか。

実際にプログラミングで「データ」を用意すると、「データ型」によって「データ」の値は様々になってきますと。

Pythonは「動的型付け言語」なので「データ型」が曖昧になってしまっている部分はありますが、ExcelファイルとかCSVファイルとかから「データ」が用意できるわけですが、それぞれのファイルの値をPythonで読み込むと、値が

  • int型
  • float型
  • String型
  • Object型

などなど、混在することは多々ありそうですと。

で、「機械学習」では、機械学習モデルを 作成するために「データ」を読み込んで学習させるわけですが、学習に使われる「データ」の値は「数値」である必要がありますと。

何故「数値」である必要があるのか?

⇧ 上記のPDFで述べられてる通り、つまるところ、「機械学習(ML:Machine Learning)」というのは「入力(数値)」を受け取って適切な「出力(数値)」に変換してくれる「関数」を見つけることであるからして、「入力」は「数値」にしておいてあげないと、上手いこと計算とかできませんと。

というわけで「データ」は「数値」にしてあげましょう、なんですが、人間社会で扱われてる「データ」の値としては、当然のことながら「数値」以外も存在するんですと。

ちなみに、「統計」においては「データ」は、

bellcurve.jp

⇧ みたいなものに分類できて、この「質的データ」ってものを「数値」にしておく必要がありますと。

で、「Pandas」を使ってる場合、「get_dummy()」メソッドってのが用意されてるので「質的データ」を「ダミー変数」にお手軽に変換できますと。

teratail.com

pandas.get_dummiesとsklearn.preprocessing.OneHotEncoderにはそういう違いがあるんですね。
「ダミー変数」「One-Hotエンコーディング」という言葉自体、中身のやることは同じという認識でよろしいのでしょうか?

Python - 【機械学習】ダミー変数とOne-Hotエンコーディングとの違いは?|teratail

⇧ とあるので、「One-Hotエンコーディング」を確認してみると、

デジタル回路において、one-hot(ワン・ホット)は1つだけHigh(1)であり、他はLow(0)であるようなビット列のことを指す。 類似のものとして、0が1つだけで、他がすべて1であるようなビット列をone-cold(ワン・コールド)と呼ぶことがある。

One-hot - Wikipedia

one-hotエンコーディングはしばしば状態機械の状態を表すのに用いられる。二進法グレイコードを使うときにはその状態を決定するためにデコードが必要だが、one-hotを使うときには不要である。というのも、one-hotではビット列のn番目のビットがHighであればn番目の状態を表していることになるからである。

One-hot - Wikipedia

⇧ みたいな感じで、「0」か「1」で表現するってことみたい。

なので、例えば「データ」に『血液型』っていう列があったとすると、

ID 血液型
0 A
1 B
2 AB
3 O
4 A
5 A

「Pandas」のget_dummy()メソッドを使うと、 

ID 血液型_A 血液型_B 血液型_AB 血液型_O
0 1 0 0 0
1 0 1 0 0
2 0 0 1 0
3 0 0 0 1
4 1 0 0 0
5 1 0 0 0

みたいな形で、「質的データ」を「数値」として表現できますと。

ただ、 「ダミー変数」にする際は、「列」を1つ減らすのが、「データサイエンス」の世界ではお決まりということなので、

get_dummy(df["血液型"], drop_first=True))    

⇧ みたいな感じで、「drop_first=True」を指定してあげるのが普通らしいです。

なので、『血液型』の例は、

ID 血液型
0 A
1 B
2 AB
3 O
4 A
5 A

「Pandas」の「get_dummy()」メソッドで「drop_first=True」を指定して実行することになり、 

ID 血液型_B 血液型_AB 血液型_O
0 0 0 0
1 1 0 0
2 0 1 0
3 0 0 1
4 0 0 0
5 0 0 0

上記のように表現するみたいね。

「血液型_A」については、「血液型_B」「血液型_AB」「血液型_O」が いずれも「0」であることによって「血液型_A」が「1」になるということを表すみたいです。

で、「ダミー変数」への変換が済んだ後に、「整然データ」にしていくという流れですかね。

ただ、「pandas.get_dummies」については、

www.haya-programming.com

⇧ 上記サイト様によりますと、「使ってはいけない」と言ってるので、実際の現場では、禁じ手的な扱いになるんですかね?

まぁ、今回は「pandas.get_dummies」使ってきますけど...

 

というわけで、「Tidy Data(整然データ)」にしてみる

まずは「データ」を用意ということで、

qiita.com

⇧ 上記サイト様を参考に、無料で利用できる「データ」を使ってみようと思いましたが、サイトのリンクとか切れてたりしたこともあり、自分でデータを用意しました。

七つの大罪 - Wikipedia」の情報を参考に「データ」を用意。

df = pd.DataFrame({
  "iD": [1, 2, 3, 4, 5, 6, 7],
  "name": ["メリオダス", "バン", "ディアンヌ", "キング/ハーレクイン", "ゴウセル", "マーリン", "エスカノール"],
  "nickName": ["憤怒の罪(ドラゴン・シン)", "強欲の罪(フォックス・シン)", "嫉妬の罪(サーペント・シン)", "怠惰の罪(グリズリー・シン)", "色欲の罪(ゴート・シン)", "暴食の罪(ボア・シン)", "傲慢の罪(ライオン・シン)"],
  "species": ["魔神族", "元人間", "巨人族", "妖精族", "人形", "不明", "人間"],  
  "power": ['{"magic": 400, "force", 990, "spirit": 2010 }', '{"magic": 1380, "force", 930, "spirit": 910 }', '{"magic": 900, "force", 1970, "spirit": 480 }', '{"magic": 3370, "force", 0, "spirit": 820 }', '{"magic": 1300, "force", 500, "spirit": 1300 }', '{"magic": 3540, "force", 70, "spirit": 1100 }', '{"magic": 5, "force", 5, "spirit": 5 }'], 
  "height": ["152", "210", "915", "160", "173", "177", "165"],
  "weight": ["50", "70", "不明", "48", "61", "57", "49"],
  "age": ["3000", "43", "750", "1300", "不明", "不明", "40"],
  "blood": ["B", "B", "O", "AB", "不明", "AB", "AB"]
})

って「データ」があったとして、まずは、「数値」でない「データ」を何とかせにゃならんので、どうにかして「カテゴリ変数」に変換せねばなのですが、

qiita.com

⇧ 上記サイト様によりますと、「Category Encoders」ってのが便利らしいんですが、「データ」の値が「JSON文字列」とかでも良しなに処理してくれるんじゃろうか?

やってみた。(事前に、pipで「Numpy」「Pandas」「Category Encoders」とかはPython仮想環境にインストールしてます。)

import pandas as pd
import numpy as np
import category_encoders as ce

df = pd.DataFrame({
  "iD": [1, 2, 3, 4, 5, 6, 7],
  "name": ["メリオダス", "バン", "ディアンヌ", "キング/ハーレクイン", "ゴウセル", "マーリン", "エスカノール"],
  "nickName": ["憤怒の罪(ドラゴン・シン)", "強欲の罪(フォックス・シン)", "嫉妬の罪(サーペント・シン)", "怠惰の罪(グリズリー・シン)", "色欲の罪(ゴート・シン)", "暴食の罪(ボア・シン)", "傲慢の罪(ライオン・シン)"],
  "species": ["魔神族", "元人間", "巨人族", "妖精族", "人形", "不明", "人間"],  
  "power": ['{"magic": 400, "force", 990, "spirit": 2010 }', '{"magic": 1380, "force", 930, "spirit": 910 }', '{"magic": 900, "force", 1970, "spirit": 480 }', '{"magic": 3370, "force", 0, "spirit": 820 }', '{"magic": 1300, "force", 500, "spirit": 1300 }', '{"magic": 3540, "force", 70, "spirit": 1100 }', '{"magic": 5, "force", 5, "spirit": 5 }'], 
  "height": ["152", "210", "915", "160", "173", "177", "165"],
  "weight": ["50", "70", "不明", "48", "61", "57", "49"],
  "age": ["3000", "43", "750", "1300", "不明", "不明", "40"],
  "blood": ["B", "B", "O", "AB", "不明", "AB", "AB"]
})

exclude_list = ["height", "weight", "age"]
list_cols = [x for x in df.columns if x not in exclude_list]
cols = ["power"]

ce_ohe = ce.OneHotEncoder(cols=cols,handle_unknown='impute')
df_ce = ce_ohe.fit_transform(df)
print("■変更前")
print(df)
print("■変更後")
print(df_ce)  

実行結果は、

■変更前
   iD        name        nickName species                                           power height weight   age blood
0   1       メリオダス   憤怒の罪(ドラゴン・シン)     魔神族   {"magic": 400, "force", 990, "spirit": 2010 }    152     50  3000     B
1   2          バン  強欲の罪(フォックス・シン)     元人間   {"magic": 1380, "force", 930, "spirit": 910 }    210     70    43     B
2   3       ディアンヌ  嫉妬の罪(サーペント・シン)     巨人族   {"magic": 900, "force", 1970, "spirit": 480 }    915     不明   750     O
3   4  キング/ハーレクイン  怠惰の罪(グリズリー・シン)     妖精族     {"magic": 3370, "force", 0, "spirit": 820 }    160     48  1300    AB
4   5        ゴウセル    色欲の罪(ゴート・シン)      人形  {"magic": 1300, "force", 500, "spirit": 1300 }    173     61    不明    不明
5   6        マーリン     暴食の罪(ボア・シン)      不明   {"magic": 3540, "force", 70, "spirit": 1100 }    177     57    不明    AB
6   7      エスカノール   傲慢の罪(ライオン・シン)      人間          {"magic": 5, "force", 5, "spirit": 5 }    165     49    40    AB
■変更後
   iD        name        nickName species  power_1  power_2  power_3  power_4  power_5  power_6  power_7 height weight   age blood
0   1       メリオダス   憤怒の罪(ドラゴン・シン)     魔神族        1        0        0        0        0        0        0    152     50  3000     B
1   2          バン  強欲の罪(フォックス・シン)     元人間        0        1        0        0        0        0        0    210     70    43     B
2   3       ディアンヌ  嫉妬の罪(サーペント・シン)     巨人族        0        0        1        0        0        0        0    915     不明   750     O
3   4  キング/ハーレクイン  怠惰の罪(グリズリー・シン)     妖精族        0        0        0        1        0        0        0    160     48  1300    AB
4   5        ゴウセル    色欲の罪(ゴート・シン)      人形        0        0        0        0        1        0        0    173     61    不明    不明
5   6        マーリン     暴食の罪(ボア・シン)      不明        0        0        0        0        0        1        0    177     57    不明    AB
6   7      エスカノール   傲慢の罪(ライオン・シン)      人間        0        0        0        0        0        0        1    165     49    40    AB    

駄目でした...

う~ん、やっぱり泥臭く人手で処理せにゃならんところは人手で処理してあげにゃならんのかね、「データ」の「前処理」は地獄ですな...

なので、「JSON文字列」とかは、一旦、自力で「JSON」の形(Pythonだと「dict」っていう型になるらしい)にしてあげる必要がありますと。

というか、値が「文字列」の「データ」なんかについては、「カテゴリ変数」に変換する前に「自然言語処理」とかしてあげておくと、「機械学習モデル」 の性能が上がるっぽいです。

話が脱線しましたが、以下でイケました。

import pandas as pd
import numpy as np
import category_encoders as ce
import json
import ast

df = pd.DataFrame({
  "iD": [1, 2, 3, 4, 5, 6, 7],
  "name": ["メリオダス", "バン", "ディアンヌ", "キング/ハーレクイン", "ゴウセル", "マーリン", "エスカノール"],
  "nickName": ["憤怒の罪(ドラゴン・シン)", "強欲の罪(フォックス・シン)", "嫉妬の罪(サーペント・シン)", "怠惰の罪(グリズリー・シン)", "色欲の罪(ゴート・シン)", "暴食の罪(ボア・シン)", "傲慢の罪(ライオン・シン)"],
  "species": ["魔神族", "元人間", "巨人族", "妖精族", "人形", "不明", "人間"],  
  "power": ['{"magic": 400, "force": 990, "spirit": 2010 }', '{"magic": 1380, "force": 930, "spirit": 910 }', '{"magic": 900, "force": 1970, "spirit": 480 }', '{"magic": 3370, "force": 0, "spirit": 820 }', '{"magic": 1300, "force": 500, "spirit": 1300 }', '{"magic": 3540, "force": 70, "spirit": 1100 }', '{"magic": 5, "force": 5, "spirit": 5 }'], 
  "height": ["152", "210", "915", "160", "173", "177", "165"],
  "weight": ["50", "70", "不明", "48", "61", "57", "49"],
  "age": ["3000", "43", "750", "1300", "不明", "不明", "40"],
  "blood": ["B", "B", "O", "AB", "不明", "AB", "AB"]
})

exclude_list = ["height", "weight", "age"]
list_cols = [x for x in df.columns if x not in exclude_list]
cols = ["power"]

# ce_ohe = ce.OneHotEncoder(cols=cols,handle_unknown='impute')
# df_ce = ce_ohe.fit_transform(df)
# print("■変更前")
# print(df)
# print("■変更後")
# print(df_ce)

# 文字列にNaNが含まれる場合は置換
def checkIncludeNaNstr(x):
  if type(x) == str and "nan" in x:
    x.replace("nan", "0")
  return x

# 文字列がJSONかどうか
def is_json_format(x):
    if type(x) != str:
      return x
    try:
        json.loads(x)
        result = convertStrToJson(x)
        return result
    except json.JSONDecodeError:
        return x

# JSON文字列からJSONへ変換
def convertStrToJson(x):
  result = ast.literal_eval(x)
  return result

# Seriesオブジェクトを作成するメソッド
def create_series(dict_value):
  return pd.Series(list(dict_value.values()), index=list(dict_value.keys()))

def checkJSON(x):
  if type(x) == json:
    return True
  else:
    return False

df = df.applymap(lambda x: checkIncludeNaNstr(x))
df = df.applymap(lambda x: is_json_format(x))
df_convert_json_to_series = df["power"].apply(lambda x: create_series(x))
df = pd.concat([df, df_convert_json_to_series], axis=1)
df.drop(columns='power', inplace=True)
# df_list = []
# for column_name, item in df.iteritems():
#   if (item.map(type) == json).count() > 0:
#     df_list.append(create_series(item))
# result_df = pd.concat([df, df_list], axis=1)
# print(df_list)
pd.set_option("display.unicode.east_asian_width", True)
print(df)

ただ、入れ子になった「dict」とかには対応できてませんが、

qiita.com

⇧ 上記サイト様のコメント欄にあるように、「再帰」処理すれば対応できそうな感じですかね。

あと、そもそもとして、実際の業務では「カテゴリ変数」にすべき「列」の洗い出しをするところとかも実施すべきだとは思いますが、おそらく、以下のようなコードでいけるかと。

# 数値に変換できない値を持つ要素を持つ列(カラム)名のリストを作成するメソッド
def getColumnByStrValueList(s, columList):
  try:
    pd.to_numeric(s, errors="raise")
  except Exception as e:
    columList.append(s.name) 

# 数値に変換できない値を要素として持つ列(カラム)名のリスト
cloumnIncludStrValueLists = []
df.apply(lambda s: getColumnByStrValueList(s, cloumnIncludStrValueLists))
cloumnIncludStrValueLists

⇧ ってな感じで、数値以外のカラム名を特定できるかと。

それでは、以下を追記で「blood」の列を「カテゴリ変数」に変換で。

allowed_vals_list = ["A", "B", "AB", "O"]
# 「血液型」以外の値は、"unknown"って文字列に置換
df.loc[~df["blood"].isin(allowed_vals_list), "blood"] = "unknown"
# カテゴリ変数に変換
df_blood = pd.get_dummies(df['blood'], drop_first=True, dummy_na=False)
df = pd.concat([df, df_blood], axis=1)
df.drop(columns='blood', inplace=True)

⇧ 気になったのは、今回「A型」が元のデータに存在しないんだけど、「drop_first=True」を指定しちゃって良いのかがよく分からん...

f:id:ts0818:20210320222636p:plain

⇧ そもそも、「weight」とか「age」で「不明」って値になってる部分ってのは、どういう値に置換すれば良いんですかね?

やっぱり、0とかにするのかな?

「平均値」とかが変な感じになりそうなんだけどな...

という感じで、「数値」に関するデータの「欠損値」の扱いが難しいんよね...

その答えはと言うと、

www.kaggle.com

⇧ かの有名な「Kaggle」で紹介されておりました。

大まかに対処法を分類すると、

  • Deletions
    • Parwise Deletion
    • Listwise Deletion/ Dropping rows
    • Dropping complete columns
  • Impulations
    • General
      • For Non Time Series
      • Time Series
    • Adavance

⇧ 上記のような感じになり、「Deletions(削除する方針)」「Impulations(値を補完する方針)」のどっちかになるっぽい。

で、 「Impulations(値を補完する方針)」でいく場合は、「For Non Time Series(日付じゃないデータと思われる)」っぽいデータなら、

  • 「constant value(定数値)」
  • 「mean(平均値)」
  • 「median(中央値)」
  • most frequent(最頻値)」

で値を補完すれば良いよ、ってことらしいんですが、ん~...そのどれを使ったら良いかが知りたいんだけどな... 

まぁ、実際問題「40歳~3000歳」なんていうハチャメチャな年齢のレンジ(幅)は現実世界では起こり得ないんだけども、ゲームとか開発してて「機械学習」するケースとか出てきた場合、どうしてるんかね?

悩ましいところなんですが、今回は「mean(平均値)」で補完することに。 

最終的には、以下のような感じで。(コードがむちゃくちゃ汚くなったけど...)

import pandas as pd
import numpy as np
import category_encoders as ce
import json
import ast
import sys

df = pd.DataFrame({
  "iD": [1, 2, 3, 4, 5, 6, 7],
  "name": ["メリオダス", "バン", "ディアンヌ", "キング/ハーレクイン", "ゴウセル", "マーリン", "エスカノール"],
  "nickName": ["憤怒の罪(ドラゴン・シン)", "強欲の罪(フォックス・シン)", "嫉妬の罪(サーペント・シン)", "怠惰の罪(グリズリー・シン)", "色欲の罪(ゴート・シン)", "暴食の罪(ボア・シン)", "傲慢の罪(ライオン・シン)"],
  "species": ["魔神族", "元人間", "巨人族", "妖精族", "人形", "不明", "人間"],  
  "power": ['{"magic": 400, "force": 990, "spirit": 2010 }', '{"magic": 1380, "force": 930, "spirit": 910 }', '{"magic": 900, "force": 1970, "spirit": 480 }', '{"magic": 3370, "force": 0, "spirit": 820 }', '{"magic": 1300, "force": 500, "spirit": 1300 }', '{"magic": 3540, "force": 70, "spirit": 1100 }', '{"magic": 5, "force": 5, "spirit": 5 }'], 
  "height": ["152", "210", "915", "160", "173", "177", "165"],
  "weight": ["50", "70", "不明", "48", "61", "57", "49"],
  "age": ["3000", "43", "750", "1300", "不明", "不明", "40"],
  "blood": ["B", "B", "O", "AB", "不明", "AB", "AB"]
})

exclude_list = ["height", "weight", "age"]
list_cols = [x for x in df.columns if x not in exclude_list]
cols = ["power"]

# ce_ohe = ce.OneHotEncoder(cols=cols,handle_unknown='impute')
# df_ce = ce_ohe.fit_transform(df)
# print("■変更前")
# print(df)
# print("■変更後")
# print(df_ce)

# 文字列にNaNが含まれる場合は置換
def checkIncludeNaNstr(x):
  if type(x) == str and "nan" in x:
    x.replace("nan", "0")
  return x

# 文字列がJSONかどうか
def is_json_format(x):
    if type(x) != str:
      return x
    try:
        json.loads(x)
        result = convertStrToJson(x)
        return result
    except json.JSONDecodeError:
        return x

# JSON文字列からJSONへ変換
def convertStrToJson(x):
  result = ast.literal_eval(x)
  return result

# Seriesオブジェクトを作成するメソッド
def create_series(dict_value):
  return pd.Series(list(dict_value.values()), index=list(dict_value.keys()))

def checkJSON(x):
  if type(x) == json:
    return True
  else:
    return False

df = df.applymap(lambda x: checkIncludeNaNstr(x))
df = df.applymap(lambda x: is_json_format(x))
df_convert_json_to_series = df["power"].apply(lambda x: create_series(x))
df = pd.concat([df, df_convert_json_to_series], axis=1)
df.drop(columns='power', inplace=True)
# df_list = []
# for column_name, item in df.iteritems():
#   if (item.map(type) == json).count() > 0:
#     df_list.append(create_series(item))
# result_df = pd.concat([df, df_list], axis=1)
# print(df_list)
# 文字列のJSONが変換されたか確認
# print(df["power"].map(type))

allowed_vals_list = ["A", "B", "AB", "O"]
df.loc[~df["blood"].isin(allowed_vals_list), "blood"] = "unknown"
df_blood = pd.get_dummies(df['blood'], drop_first=True, dummy_na=False)
df = pd.concat([df, df_blood], axis=1)
df.drop(columns='blood', inplace=True)
pd.set_option('display.max_rows', None)
#pd.set_option("display.unicode.ambiguous_as_wide", True)
#pd.set_option("display.html.table_schema", True)
#print(df.to_csv(sys.stdout, sep=" "))
#print(df.to_string(justify="left", force_unicode=True))
pd.set_option("display.unicode.east_asian_width", True)
print(df)

# 数値に変換できない値を持つ要素を持つ列(カラム)名のリストを作成するメソッド
def getColumnByStrValueList(s, columnList):
  try:
    pd.to_numeric(s, errors="raise")
  except Exception as e:
    columnList.append(s.name)

# 数値に変換できない値を要素として持つ列(カラム)名のリスト
cloumnIncludStrValueLists = []
df.apply(lambda s: getColumnByStrValueList(s, cloumnIncludStrValueLists))
print(cloumnIncludStrValueLists)

# 数値と文字列が混在してる要素かそうでないかを判定
def checkAllCloumnsValueByStr(s, checkDict, columnList):
  columnList = []
  for value in s:
    if type(value) == str:
      columnList.append(True)
    else:
      columnList.append(False)
  checkDict[s.name] = columnList

# DataFrameの列の各要素の値のデータ型を確認する
checkColumnsDict = {}
checkClumnValueTypeList = []
df[cloumnIncludStrValueLists].apply(lambda s: checkAllCloumnsValueByStr(s, checkColumnsDict, checkClumnValueTypeList))
print(checkColumnsDict)

# DataFrameの列で、要素の値のデータ型が全て同じもの(今回はstr型)のカラム名のリストを作成
onlyStrValueColumnList = []
for key in checkColumnsDict:
  if len(list(set(checkColumnsDict[key]))) == 1:
    onlyStrValueColumnList.append(key)
print(onlyStrValueColumnList)

# 数値と文字列が混在する列の値のうち、文字列はNaN値(欠損値)に置き換え
def replaceStrToNan(s):
  for index in s.index:   
    if type(s[index]) == str:
      s[index] = np.nan

# SettingWithCopyWarning対策のため、一旦、新しい変数に退避
df_tmp = df[[i for i in list(df.columns) if i not in onlyStrValueColumnList]].copy()
# 文字列はNaN値(欠損値)に置き換え
df_tmp.apply(lambda s: replaceStrToNan(s))
print(df_tmp)
# NaN値(欠損値)を列の平均値で置き換え
df_tmp = df_tmp.fillna(df_tmp.mean().round().astype(int))
# 分割してたDataFrame同士を横(axis=1)で結合
df = pd.concat([df[onlyStrValueColumnList], df_tmp], axis=1)
#df[[i for i in list(df.columns) if i not in onlyStrValueColumnList]].fillna(df.mean(), inplace=True)
# 結局、マジックナンバー(ここでは、10)使っちゃったけど、「Tidy Data(整然データ)」に置き換え
df = pd.melt(df, id_vars=df.columns.values[:10], var_name="blood", value_name="blood_value")
print(df)

⇧ で実行してみると、

                   name                      nickName species  iD  height  weight   age  magic  force  spirit    blood  blood_value
0            メリオダス    憤怒の罪(ドラゴン・シン)  魔神族   1     152      50  3000    400    990    2010        B            1
1                  バン  強欲の罪(フォックス・シン)  元人間   2     210      70    43   1380    930     910        B            1
2            ディアンヌ  嫉妬の罪(サーペント・シン)  巨人族   3     915      56   750    900   1970     480        B            0
3   キング/ハーレクイン  怠惰の罪(グリズリー・シン)  妖精族   4     160      48  1300   3370      0     820        B            0
4              ゴウセル      色欲の罪(ゴート・シン)    人形   5     173      61  1027   1300    500    1300        B            0
5              マーリン        暴食の罪(ボア・シン)    不明   6     177      57  1027   3540     70    1100        B            0
6          エスカノール    傲慢の罪(ライオン・シン)    人間   7     165      49    40      5      5       5        B            0
7            メリオダス    憤怒の罪(ドラゴン・シン)  魔神族   1     152      50  3000    400    990    2010        O            0
8                  バン  強欲の罪(フォックス・シン)  元人間   2     210      70    43   1380    930     910        O            0
9            ディアンヌ  嫉妬の罪(サーペント・シン)  巨人族   3     915      56   750    900   1970     480        O            1    

f:id:ts0818:20210331194431p:plain

⇧ 上記のような感じのデータになりましたと。

カラムの順番がめちゃくちゃになってるけども...

 

Tidy Data(整然データ)をどうやって機械学習に使えば?

で、試行錯誤の果てに「Tidy Data(整然データ)」が無事に作成できました、めでたしめでたし...で終わらないのが「データ分析」ですと。

というか、 「Tidy Data(整然データ)」をどうやって「機械学習」で使えるようにするのよ?って情報が見当たらんのですわ...

何を気にしてるかというと、「機械学習」で扱う「データ」って「数値」である必要があるはずじゃないですか?

機械学習」で扱う「データ」って「数値」じゃなきゃダメよ、という前提がある以上、「Tidy Data(整然データ)」にすると確かに「データ」は見やすい形にはなるけれど、「数値」じゃない値のカラムができちゃったよね?

どうすれば良いの?

ここで衝撃の事実が...

sorabatake.jp

f:id:ts0818:20210308195320p:plain

機械学習ではカテゴリ変数を扱うことが苦手なために、one-hot encodingという手法を用いてカテゴリ変数を0と1のダミー変数に変換することがあります。そのため上で示したような値が変数として用いられることがあります。つまり、データの「整った形」とは、どのような解析をするのかで変化することがあります。重要なことは、自分の解析に合わせて、データの構造を自由自在に変化させていくことです。

【データサイエンス入門】Pythonでテーブルデータを扱いたい人のためのライブラリまとめ | 宙畑

⇧ つまり、「Tidy Data(整然データ)」は「機械学習」で使えないデータになり得る可能性があるという...

ここで、「Tidy Data(整然データ)」について今一度確認。 

id.fnshr.info

整然データは、データセットを構造化する標準的手法を提供するため、分析者や計算機が必要な変数を抽出することを容易にする

【翻訳】整然データ|Colorless Green Ideas

⇧ え、え~っと... ...

 

というわけで、残念ながら、「Tidy Data(整然データ)」にしてはいけない時もあるってことなんですかね?

「入力データ」については、「カテゴリ変数」へ変換後は「Tidy Data(整然データ)」に変換するのは諦めて、「機械学習モデル」に投入していくことになるのかな?

分からんことが多過ぎるな...

今回はこのへんで。