20
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python&機械学習&データ分析なプログラミングにおけるTips

Last updated at Posted at 2022-04-25

これは何?

Python&機械学習&データ分析なプログラミングをする筆者(@daikikatsuragawa)のこれまでの経験に基づくTipsです。個人的に後に確認するためにまとめ、せっかくなので公開します。あわよくば「“もっと良い書き方”がありますよ。」というコメントや編集リクエストを期待しています。大歓迎です。主に以下について書きます。

  • コードスニペット
  • リファクタリング例
  • 豆知識

※随時更新します。

本記事を執筆したきっかけ
本記事を紹介してくださった記事

コードスニペット

インスタンスをファイルとして保存&復元

プログラミングにおいて扱っているインスタンスをファイルとして保存したい場面があります。pickleによりオブジェクトをファイルとして保存&復元することが可能です。以下などに有用です。

  • 学習済みモデル
  • 前処理を実施するインスタンス(例:MinMaxScaler
学習済みモデルの作成
import pandas as pd
import seaborn as sns
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder


sample_df = sns.load_dataset("titanic")
sample_df = sample_df.dropna()

# カテゴリ変数に対してラベルエンコーディング
for column in ["sex", "embarked", "class",  "who", "deck", "embark_town", "alive"]:
  le = LabelEncoder()
  sample_df[column] = le.fit_transform(sample_df[column].values)

# 欠損値のゼロ埋め
sample_df = sample_df.fillna(0)

X = sample_df.drop(columns="survived")
y = sample_df["survived"]

train_x, test_x, train_y, test_y = train_test_split(X, y, test_size=0.2, random_state=123)

model = LogisticRegression(random_state=123)
model.fit(train_x, train_y)
modelをmodel.pickleという名前のファイルとして保存
import pickle


pickle.dump(model, open("model.pickle", "wb"))
model.pickleという名前のファイル名を復元(ロード)
restored_model = pickle.load(open("model.pickle", "rb"))

元のモデルと復元したモデルの振る舞いは同じです。

元のモデルと復元したモデルの振る舞いは同じ
(model.predict(test_x) == restored_model.predict(test_x)).all()
元のモデルと復元したモデルの振る舞いは同じ(出力結果)
True

※拡張子を.pickleにするか.pklにするか。あらかじめプロジェクト内で決めておくと良いです。

前処理を実施するインスタンス(MinMaxScalerなど)もモデルと同様にトレーニングデータで基準を決定します。テストデータはそのインスタンスの基準に基づき変換されます。その基準(MinMaxScalerの場合、最大値・最小値といったパラメータ)の管理ができる場合は問題ないです。しかし、困難な状況だったりします。扱いはモデルと類似するので、モデルと同様に“ファイルとして保存&復元”します。

python:min_max_scalerの生成
from sklearn.preprocessing import MinMaxScaler


min_max_scaler = MinMaxScaler()
min_max_scaler.fit(train_x)
min_max_scalerをscaler.pickleという名前のファイルとして保存
pickle.dump(min_max_scaler, open("scaler.pickle", "wb"))
scaler.pickleという名前のファイル名を復元(ロード)
restored_min_max_scaler = pickle.load(open("scaler.pickle", "rb"))
元の前処理と復元した前処理の振る舞いは同じ
(min_max_scaler.transform(test_x) == restored_min_max_scaler.transform(test_x)).all()
元のモデルと復元したモデルの振る舞いは同じ(出力結果)
True

学習済みモデル(scikit-learn)から対応する説明変数を取得

以下により学習済みモデル(scikit-learn)から対応する説明変数を取得することが可能です。

学習済みモデル(scikit-learn)から対応する説明変数を取得
# 上記“学習済みモデルの作成”まで実施したとします。
model.feature_names_in_.tolist()
学習済みモデル(scikit-learn)から対応する説明変数を取得(出力)
['pclass',
 'sex',
 'age',
 'sibsp',
 'parch',
 'fare',
 'embarked',
 'class',
 'who',
 'adult_male',
 'deck',
 'embark_town',
 'alive',
 'alone']

学習済みモデルの想定より多くの説明変数を持つデータフレームへの対応

学習済みモデルに対して、より多くの説明変数を持つデータフレームを入力するとValueErrorが生じます。具体的に、以下は説明変数が20件の学習済みモデルに対して、説明変数が21件のデータフレームを入力し、失敗する例です。“学習済みロジスティック回帰モデル(scikit-learn)から対応する説明変数を取得”を参考にしてモデルに入力する際に説明変数を絞り込むと良いと考えられます。

Q.学習済みモデルの想定より多くの説明変数が存在している場合、どうなる?
# 上記“学習済みモデルの作成”まで実施したとします。
test_x["unnecessary_column"] = 0
model.predict(test_x)
Q.学習済みモデルの想定より多くの説明変数が存在している場合、どうなる?/A.ValueErrorが発生
省略
ValueError: X has 15 features, but RandomForestClassifier is expecting 14 features as input.
対象カラムの絞り込みによる学習済みモデルの想定より多くの説明変数への対応(無視)
# 上記“学習済みモデルの作成”まで実施したとします。
test_x["unnecessary_column"] = 0
model.predict(test_x[model.feature_names_in_.tolist()])

参考までに以下のような記述によりデータフレームのカラムをリスト(["age", "alive", "alone"])で絞り込むことが可能です。

参考:対象カラムの絞り込み
test_x[["age", "alive", "alone"]]

このようなデータフレームの扱い方については以下のチートシートが参考として有用です。

不均衡データに対するオーバーサンプリング(SMOTE)

不均衡データに対してSMOTEなどによるオーバーサンプリングが有用です。

準備&オーバーサンプリング前のデータの確認
import pandas as pd
import seaborn as sns


sample_df = sns.load_dataset("titanic")
sample_df.value_counts("survived")

# カテゴリ変数に対してラベルエンコーディング
for column in ["sex", "embarked", "class",  "who", "deck", "embark_town", "alive"]:
  le = LabelEncoder()
  sample_df[column] = le.fit_transform(sample_df[column].values)

# 欠損値のゼロ埋め
sample_df = sample_df.fillna(0)

X = sample_df.drop(columns="survived")
y = sample_df["survived"]

y.value_counts()

0が549件、1が342件です。

準備&オーバーサンプリング前のデータの確認(出力)
0    549
1    342
Name: survived, dtype: int64

SMOTEによるオーバーサンプリングを実施します。

SMOTEによるオーバーサンプリング&データの確認
from imblearn.over_sampling import SMOTE


smote = SMOTE()
resampled_X, resampled_y = smote.fit_resample(X, y)

resampled_y.value_counts()

オーバーサンプリング前は0が549件、1が342件だったのに対して、1が549件になっています。

SMOTEによるオーバーサンプリング&データの確認(出力)
0    549
1    549
Name: survived, dtype: int64

クラスの偏りを考慮したロジスティック回帰

不均衡データ、つまりクラスの偏りが生じるようなデータを扱う場合は、何らかの対処をすることが望まれます。このとき、手法がロジスティック回帰だと、重みを設定することができます。細かい設定も可能ですが、簡単な方法として、LogisticRegressionsklearn.linear_model.LogisticRegression)のキーワード引数のひとつであるclass_weight"balanced"を指定するといったものがあります。

クラスの偏りを考慮したロジスティック回帰
import pandas as pd
import seaborn as sns
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder


sample_df = sns.load_dataset("titanic")
sample_df = sample_df.dropna()

# カテゴリ変数に対してラベルエンコーディング
for column in ["sex", "embarked", "class",  "who", "deck", "embark_town", "alive"]:
  le = LabelEncoder()
  sample_df[column] = le.fit_transform(sample_df[column].values)

# 欠損値のゼロ埋め
sample_df = sample_df.fillna(0)

X = sample_df.drop(columns="survived")
y = sample_df["survived"]

train_x, test_x, train_y, test_y = train_test_split(X, y, test_size=0.2, random_state=123)

model = LogisticRegression(class_weight="balanced", random_state=123)
model.fit(train_x, train_y)

特定のカラムにおけるそれぞれの要素の出現頻度を出力

データフレームに対して、groupby()メソッドおよびsize()メソッドにより、特定のカラムにおけるそれぞれの要素の出現頻度を出力する。

特定のカラムにおけるそれぞれの要素の出現頻度を出力
sample_df.groupby("survived").size()
特定のカラムにおけるそれぞれの要素の出現頻度を出力(出力)
survived
0    549
1    342
dtype: int64

pandas.DataFrameのカラム名の変更方法3選

pandas.DataFrameのカラム名の変更方法3選を記述します。まずはダミーデータを生成します。

準備(ダミーデータの作成)
import pandas as pd


df = pd.DataFrame([["A", "B"],["C", "D"]])
print(df)

以下のように出力されます。カラムが設定されておらず01と表示されております。

カラム名(変更前)
   0  1
0  A  B
1  C  D

上記のダミーデータのカラム名をc1c2とする方法を以下に列挙します。

カラム名(変更後)
   c1    c2
0   A     B
1   C     D
pandas.DataFrameのカラム名の変更方法(その①)
df.columns=["c1", "c2"]
pandas.DataFrameのカラム名の変更方法(その②)
df.set_axis(["c1", "c2"], axis="columns", inplace=True)
pandas.DataFrameのカラム名の変更方法(その③)
df.rename(columns = {0: "c1", 1: "c2",}, inplace=True)

個人的には、なるべく下の方を使いたいと思っています。③はカラムの入れ違いを防ぐことから好んでいます。①がデータフレームの属性(今回はcolumns)を直接変更しているのに対して、②はメソッドを利用して属性を変更していることから(個人的な好みに基づき)好んでいます。

pydanticによるバリデーション

pydanticは、定義したクラスに対して、型アノテーションなどを利用したバリデーションを実現するライブラリです。

pydanticのインストール
!pip install pydantic

例えば以下のようにUserクラスを作成できます。このUserクラス以下を受け取ります。

  • ユーザーID(user_id):5〜15の文字列(str型)
    • TwitterなどSNSのイメージ
  • 年齢(age):18以上の整数(int型)

これにより、条件を満たさないUserクラスのインスタンスは存在しません。また、user_idは全て小文字に変換するような設定(to_lower=True)をしています。

pydanticによるUserクラスの生成
from pydantic import BaseModel, Field, conint, constr


class User(BaseModel):
    user_id: constr(
        to_lower=True,
        regex=r'\w+', # [a-zA-Z_0-9]+
        min_length=5,
        max_length=15,
    )
    age: conint(ge=18)
インスタンスを生成①
user = User(user_id="Abcdefg", age=18)

以下でもインスタンスを生成できます。

インスタンスを生成②
user_dict = {
    "user_id":"Abcdefg",
    "age":18,
}

user = User(**user_dict)

値のやりとりのためにdict型、JSON(str型)への変換もできます。

dict型への変換
user.dict()
dict型への変換(出力)
{'age': 18, 'user_id': 'abcdefg'}
JSON(str型)への変換
user.json()
JSON(str型)への変換(出力)
'{"user_id": "abcdefg", "age": 18}'

pydanticによる環境変数の取得

pydanticでは環境変数の管理も可能です。以下のように取得が可能です。

pydanticのインストール
!pip install pydantic
環境変数の設定(@Google Colaboratory)
%env XXX_NAME=dummy
%env STAGE=staging
pydanticによるSettingsクラス&インスタンスの生成(バリデーション済み)
from pydantic import BaseSettings
from typing_extensions import Literal


class Settings(BaseSettings):
    xxx_name: str
    stage: Literal["development", "staging", "production"]

settings = Settings()

panderaによるデータフレームのバリデーション

panderaはpandasのデータフレーム(pandas.DataFrame)のバリデーションを実現します。利用方法としてはpydanticと似ているところもあります。以前、別記事でしっかりと書いたためこちらについてはリンクを貼っておきます。ぜひ、ご覧ください。

データフレーム⇆エクセルファイル(.xlsx)

扱うデータが格納されているファイルはCSVとは限りません。エクセルファイル(.xlsx)の場合もあります。エクセルによりエクスポートしたり…という方法もありますが、なるべく手による作業は減らしてPythonで書いた方が後々便利です。複数シートを含むエクセルのファイルを出力する場面もあるかと思い、データフレーム⇆エクセルファイル(.xlsx)を紹介します。

データフレーム←エクセルファイル
import pandas as pd


input_xlsx_path = "input.xlsx"
sample_dfs = pd.read_excel(input_xlsx_path, sheet_name=None)

# OrderedDict型なのでシート名を指定することで対象のデータフレームを参照可能
sample_dfs["sheet1"]
データフレーム→エクセルファイル
output_xlsx_path = "output.xlsx"
with pd.ExcelWriter(output_xlsx_path) as writer:
    sample_df1.to_excel(writer, sheet_name="sheet1")
    sample_df2.to_excel(writer, sheet_name="sheet2")
データフレーム→エクセルファイル(リストや辞書を活用すると繰り返しによる格納も可能)
output_xlsx_path = "output.xlsx"
with pd.ExcelWriter(output_xlsx_path) as writer:
    for sheet_name in sample_dfs:
      sample_dfs[sheet_name].to_excel(writer, sheet_name=sheet_name)

matplotlibのグラフを高解像度で保存

matplotlibで生成したグラフをplt.show()で描画したりとすることがあるかと思います。それをファイルとして保存したい、可能であれば高解像度で保存したいという場面があるかと思います。以下はその例です。

japanize-matplotlibのインポート
!pip install japanize-matplotlib
matplotlibのグラフを高解像度で保存
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib


x = np.array(["A", "B", "C", "D", "E"])
height = np.array([10, 20, 30, 40, 50])

plt.bar(x=x, height=height)
plt.title("タイトル")

plt.savefig("/content/sample.png", dpi=300, format="png", bbox_inches="tight")

上記により以下のようなファイルが生成されます。

  • ファイル名:sample.png
  • dpi(解像度):300
  • 拡張子:png

sample.png

bbox_inches="tight"は余白に関する設定です。

データフレームのマークダウンへの変換

データフレームの出力をドキュメントに残したい場合、データフレームのマークダウン(表)へと変換するのがオススメです。画像のキャプチャーの場合、容量、保存場所(それに伴うリンク切れ)などが課題になり兼ねます。

データフレームのマークダウンへの変換
import pandas as pd
import seaborn as sns


sample_df = sns.load_dataset("titanic")
sample_df.head().to_markdown()
表(マークダウン)
|    |   survived |   pclass | sex    |   age |   sibsp |   parch |    fare | embarked   | class   | who   | adult_male   | deck   | embark_town   | alive   | alone   |
|---:|-----------:|---------:|:-------|------:|--------:|--------:|--------:|:-----------|:--------|:------|:-------------|:-------|:--------------|:--------|:--------|
|  0 |          0 |        3 | male   |    22 |       1 |       0 |  7.25   | S          | Third   | man   | True         | nan    | Southampton   | no      | False   |
|  1 |          1 |        1 | female |    38 |       1 |       0 | 71.2833 | C          | First   | woman | False        | C      | Cherbourg     | yes     | False   |
|  2 |          1 |        3 | female |    26 |       0 |       0 |  7.925  | S          | Third   | woman | False        | nan    | Southampton   | yes     | True    |
|  3 |          1 |        1 | female |    35 |       1 |       0 | 53.1    | S          | First   | woman | False        | C      | Southampton   | yes     | False   |
|  4 |          0 |        3 | male   |    35 |       0 |       0 |  8.05   | S          | Third   | man   | True         | nan    | Southampton   | no      | True    |

Google Colaboratoryのセル内のコードの整形

blackやyapfなどを使用するとPythonのコードを整形することができます。ただし、Google Colaboratoryなどのノートブック形式では工夫が必要です。以下にその対処法が紹介されています。

文字列型で表現されたカンマ入りの数字を数値型に変換する際のこだわり

4桁以上の数字をデータとして扱う場合、4桁ごとにカンマが入っている場合があります。Pythonではこの「文字列型で表現されたカンマ入りの数字」を数値型に変換するには以下のようにカンマを除去する必要があります。

文字列型で表現されたカンマ入りの数字を数値型に変換
thousand_str = '1,000'
thousand_str = thousand_str.replace(',', '')
int(thousand_str) # 変換可能
float(thousand_str) # 変換可能
print(thousand_str)
変換後の文字列
1000

上記でも十分ですが、個人的なこだわりを紹介します。個人的に、「カンマがあった」という情報を除去してしまうことに抵抗があります。そこで、以下のように除去せず、アンダーバー(_)に置換しています。Pythonでは、アンダーバーを含む文字列型の数字を数値として変換することが可能です。

文字列型で表現されたカンマ入りの数字を数値型に変換(こだわり版)
thousand_str = '1,000'
thousand_str = thousand_str.replace(',', '_')
int(thousand_str) # 変換可能
float(thousand_str) # 変換可能
print(thousand_str)
変換後の文字列(こだわり版)
1_000

リファクタリング例

リスト内包表記

リスト内包表記を使いこなすと以下が実現されます。

  • 行数の減少
  • リストの初期化/append()の削減
  • 不要な変数(一時的な変数)の削減

ただし、覚えるコストもあり、リファクタリング例としてまとめます。

リスト内包表記①
- y_list = []
- for element in y.tolist():
-     y_list.append(bool(element))
+ y_list = [bool(element) for element in y.tolist()]
リスト内包表記②
- positibe_y_list = []
- for element in y.tolist():
-     if element == 1:
-         positibe_y_list.append(True)
+ positibe_y_list = [True for element in y.tolist() if element == 1]
リスト内包表記③
- y_list = []
- for element in y.tolist():
-     if element == 1:
-         y_list.append(True)
-     else:
-         y_list.append(False)
+ y_list = [True if element == 1 else False for element in y.tolist()]

辞書内包表記

リスト内包表記と同様に辞書内包表記も存在しています。

辞書内包表記①
- y_dict = {}
- for element in y.tolist():
-     y_dict[element] = bool(element)
+ y_dict = {element : bool(element) for element in y.tolist()}
リスト内包表記②
- positibe_y_dict = {}
- for element in y.tolist():
-     if element == 1:
-         positibe_y_dict[element] = True
+ positibe_y_dict = {element : True for element in y.tolist() if element == 1}
リスト内包表記③
- y_dict = {}
- for element in y.tolist():
-     if element == 1:
-         y_dict[element] = True
-     else:
-         y_dict[element] = False
+ y_dict = {element : True if element == 1 else False for element in y.tolist()}

辞書型における「キー自体が存在する」かつ「キーの値がある(is not None)」をひとつの条件式で表現

以下のような処理があったとします。これに対し、辞書.get()により値がない時のデフォルト値を設定すると、比較を一つで表現できます。orandの記述が不要になります。

「キー自体が存在する」かつ「キーの値がある(is not None)」を確認
  tests =[
          {"キー" : "バリュー"},
          {"キー" : None},
          {}
  ]

  for test in tests:
    # 括弧はなくても良いが理解を容易にするために記述
-   if ("キー" in test) and (test["キー"] is not None):
+   if test.get("キー", None) is not None:
      print("not None")
    else:
      print("None")

型の比較はtype()==ではなくisinstance()を使うべし!

条件式など、型を比較する場面があります。そのような場合、次の2つの方法が利用できます。

    • type()==
  • isinstance()

Flake8 Rules(E721)では以下のように記述されています。

Do not compare types, use 'isinstance()' (E721)
A object should be be compared to a type by using isinstance. This is because isinstance can handle subclasses as well.
引用元:https://www.flake8rules.com/rules/E721.html

つまり、2つの方法のうち、後者のisinstance()を使う方が望ましいようです。上記に従うと以下のようなリファクタリングを実施することとなります。

type()と==による型の比較→isinstance()による型の比較
  sample_str = "サンプル文字列"
- if type(sample_str) == str:
+ if isinstance(sample_str, str):
      pass

複数の型と比較する場合は以下のように表現します。isinstance()は一つで、第二引数に複数の型で構成されるタプルを入力します。

type()と==による型の比較→isinstance()による型の比較(複数の型と比較する場合)
  sample_num = 1 # もしくは1.0
- if (type(sample_num) == int) or (type(sample_num) == float):
+ if isinstance(sample_num, (int, float)):
      pass

集合の要素の有無の確認をシンプルに

リストなどで「集合の要素の有無の確認」を実施する必要がある場合、len()により要素の数を特定し、それが0より大きいか(>0)といった方法で確認が可能です。実はもっとシンプルな書き方にできます。以下はPythonの真理値テスト(Truth Value Testing)についての記述です。

empty sequences and collections: '', (), [], {}, set(), range(0)
引用元:https://docs.python.org/3/library/stdtypes.html?ref=morioh.com&utm_source=morioh.com#truth-value-testing

つまり、空のシーケンスとコレクション(例:''()[]{}set()range(0))がFalseと評価されます。したがって、以下のような記述が可能です。

集合の要素の有無の確認をシンプルに
      empty_list = []
-     if len(empty_list) > 0:
+     if empty_list:
          pass

※集合以外にも真理値テスト可能なものがあるので、気になった方は是非チェックを!

同じ変数を扱う複数の比較ではin演算子を使うべし!

in演算子を使うと、ある変数を複数の値と比較することができます。この時、比較する集合はlistでも構いませんが、以下の理由からsetを使うことが望ましい気がします。

  • 重複が不要な集合はlistではなくsetを使った方が自然?
    • ドメインにおける制約をプログラムに実装すると考えると自然なこと
  • 検索が高速になる(らしい)

特に以下のような場合、listよりsetが望ましいでしょう。

同じ変数を扱う複数の比較ではin演算子を使うべし!
-   if (janken == "グー") or (janken == "チョキ") or (janken == "パー"):
+   if janken in {"グー", "チョキ", "パー"}:
        pass

豆知識

要素が一つであってもタプルにはカンマ(,)が必要(一部例外あり)

要素が一つのタプルの初期化には注意が必要です。以下のような記述の場合、タプルとして定義したtuple_with_one_elementの型がintになってしまっています。例えば、他のメソッド、関数の入力として、要素が一つのタプルを選択する場合、期待していない型の値を渡してしまうことになり、エラーが生じることでしょう。

NG例:要素が一つのタプルの初期化
tuple_with_one_element = (0)
print(type(tuple_with_one_element))
NG例:要素が一つのタプルの初期化(出力)
<class 'int'>

要素が一つのタプルの初期化は以下のように記述します。カンマ(,)を記述します。

OK例:要素が一つのタプルの初期化
tuple_with_one_element = (0,)
print(type(tuple_with_one_element))
OK例:要素が一つのタプルの初期化(出力)
<class 'tuple'>

その理由はPythonのドキュメントより、以下のように記述されています。

なお、タプルを作るのはカンマであり、丸括弧ではありません。
引用元:組み込み型 — Python 3.10.4 ドキュメント(2022/04/27)

余談ですが、以下のような記述(tuple()+リスト型を入力)も可能です。上述したOK例:要素が一つのタプルの初期化と同じ振る舞いになります。こちらはカンマ(,)は不要ですね。

OK例:要素が一つのタプルの初期化(tuple()+リスト型を入力)
tuple_with_one_element = tuple([0])
print(type(tuple_with_one_element))

一つの条件式でA以上B未満の表現が可能

Pythonでは一つの条件式で例えばA以上B未満の表現が可能です。同様に超過、以下も指定が可能です。例えば、ある変数n(50)が0以上100未満であることを判定したい場合、以下のような記述が可能です。

ある変数nが0以上100未満であることを判定
n = 50

if 0 <= n < 100:
    print("OK")
ある変数nが0以上100未満であることを判定(出力)
OK

以下のような数学の表現をそのまま使える点で好んでいます。

0 \leqq n < 100

ちなみに、以下の対義語が以上なのに対して、未満の対義語は超過です。ただし「B未満」に対して「A超過」とは言わない気がするので使うとしたら「A超」でしょうか。「Aより大きい」が無難な気がしました🤔

20
37
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?