これは何?
Python&機械学習&データ分析なプログラミングをする筆者(@daikikatsuragawa)のこれまでの経験に基づくTipsです。個人的に後に確認するためにまとめ、せっかくなので公開します。あわよくば「“もっと良い書き方”がありますよ。」というコメントや編集リクエストを期待しています。大歓迎です。主に以下について書きます。
- コードスニペット
- リファクタリング例
- 豆知識
※随時更新します。
本記事を執筆したきっかけ
本記事を紹介してくださった記事
- 【毎日自動更新】データに関する記事を書こう! LGTMランキング!(2022/04/25 10:00)
- 【毎日自動更新】新人プログラマ応援 - みんなで新人を育てよう!(2022年04月) LGTMランキング!(2022/04/25 12:02)
- 【初心者】Qiita デイリー LGTM 数ランキング【自動更新】(2022/04/25 22:00)
- 【初心者】Qiita 週間 LGTM 数ランキング【自動更新】(2022/04/25 22:00)
- 【Python】Qiita 週間 LGTM 数ランキング【自動更新】(2022/04/26 02:00)
- 【Python】Qiita デイリー LGTM 数ランキング【自動更新】(2022/04/26 02:00)
- Qiita デイリー LGTM 数ランキング【自動更新】(2022/04/26 03:01)
- Qiita 週間 LGTM 数ランキング【自動更新】(2022/04/27 21:00)
- Qiita週間ストック数ランキング【自動更新】(2022/04/28 03:31)
コードスニペット
インスタンスをファイルとして保存&復元
プログラミングにおいて扱っているインスタンスをファイルとして保存したい場面があります。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)
import pickle
pickle.dump(model, open("model.pickle", "wb"))
restored_model = pickle.load(open("model.pickle", "rb"))
元のモデルと復元したモデルの振る舞いは同じです。
(model.predict(test_x) == restored_model.predict(test_x)).all()
True
※拡張子を.pickle
にするか.pkl
にするか。あらかじめプロジェクト内で決めておくと良いです。
前処理を実施するインスタンス(MinMaxScaler
など)もモデルと同様にトレーニングデータで基準を決定します。テストデータはそのインスタンスの基準に基づき変換されます。その基準(MinMaxScaler
の場合、最大値・最小値といったパラメータ)の管理ができる場合は問題ないです。しかし、困難な状況だったりします。扱いはモデルと類似するので、モデルと同様に“ファイルとして保存&復元”します。
from sklearn.preprocessing import MinMaxScaler
min_max_scaler = MinMaxScaler()
min_max_scaler.fit(train_x)
pickle.dump(min_max_scaler, open("scaler.pickle", "wb"))
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)から対応する説明変数を取得することが可能です。
# 上記“学習済みモデルの作成”まで実施したとします。
model.feature_names_in_.tolist()
['pclass',
'sex',
'age',
'sibsp',
'parch',
'fare',
'embarked',
'class',
'who',
'adult_male',
'deck',
'embark_town',
'alive',
'alone']
学習済みモデルの想定より多くの説明変数を持つデータフレームへの対応
学習済みモデルに対して、より多くの説明変数を持つデータフレームを入力するとValueErrorが生じます。具体的に、以下は説明変数が20件の学習済みモデルに対して、説明変数が21件のデータフレームを入力し、失敗する例です。“学習済みロジスティック回帰モデル(scikit-learn)から対応する説明変数を取得”を参考にしてモデルに入力する際に説明変数を絞り込むと良いと考えられます。
# 上記“学習済みモデルの作成”まで実施したとします。
test_x["unnecessary_column"] = 0
model.predict(test_x)
(省略)
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によるオーバーサンプリングを実施します。
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件になっています。
0 549
1 549
Name: survived, dtype: int64
クラスの偏りを考慮したロジスティック回帰
不均衡データ、つまりクラスの偏りが生じるようなデータを扱う場合は、何らかの対処をすることが望まれます。このとき、手法がロジスティック回帰だと、重みを設定することができます。細かい設定も可能ですが、簡単な方法として、LogisticRegression
(sklearn.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)
以下のように出力されます。カラムが設定されておらず0
、1
と表示されております。
0 1
0 A B
1 C D
上記のダミーデータのカラム名をc1
、c2
とする方法を以下に列挙します。
c1 c2
0 A B
1 C D
df.columns=["c1", "c2"]
df.set_axis(["c1", "c2"], axis="columns", inplace=True)
df.rename(columns = {0: "c1", 1: "c2",}, inplace=True)
個人的には、なるべく下の方を使いたいと思っています。③はカラムの入れ違いを防ぐことから好んでいます。①がデータフレームの属性(今回はcolumns
)を直接変更しているのに対して、②はメソッドを利用して属性を変更していることから(個人的な好みに基づき)好んでいます。
pydanticによるバリデーション
pydanticは、定義したクラスに対して、型アノテーションなどを利用したバリデーションを実現するライブラリです。
!pip install pydantic
例えば以下のようにUserクラスを作成できます。このUserクラス以下を受け取ります。
- ユーザーID(
user_id
):5〜15の文字列(str型)- TwitterなどSNSのイメージ
- 年齢(
age
):18以上の整数(int型)
これにより、条件を満たさないUserクラスのインスタンスは存在しません。また、user_idは全て小文字に変換するような設定(to_lower=True
)をしています。
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型)への変換もできます。
user.dict()
{'age': 18, 'user_id': 'abcdefg'}
user.json()
'{"user_id": "abcdefg", "age": 18}'
pydanticによる環境変数の取得
pydanticでは環境変数の管理も可能です。以下のように取得が可能です。
!pip install pydantic
%env XXX_NAME=dummy
%env STAGE=staging
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()
で描画したりとすることがあるかと思います。それをファイルとして保存したい、可能であれば高解像度で保存したいという場面があるかと思います。以下はその例です。
!pip install japanize-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
※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()
により値がない時のデフォルト値を設定すると、比較を一つで表現できます。or
、and
の記述が不要になります。
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()
を使う方が望ましいようです。上記に従うと以下のようなリファクタリングを実施することとなります。
sample_str = "サンプル文字列"
- if type(sample_str) == str:
+ if isinstance(sample_str, str):
pass
複数の型と比較する場合は以下のように表現します。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
が望ましいでしょう。
- if (janken == "グー") or (janken == "チョキ") or (janken == "パー"):
+ if janken in {"グー", "チョキ", "パー"}:
pass
豆知識
要素が一つであってもタプルにはカンマ(,
)が必要(一部例外あり)
要素が一つのタプルの初期化には注意が必要です。以下のような記述の場合、タプルとして定義したtuple_with_one_element
の型がintになってしまっています。例えば、他のメソッド、関数の入力として、要素が一つのタプルを選択する場合、期待していない型の値を渡してしまうことになり、エラーが生じることでしょう。
tuple_with_one_element = (0)
print(type(tuple_with_one_element))
<class 'int'>
要素が一つのタプルの初期化は以下のように記述します。カンマ(,
)を記述します。
tuple_with_one_element = (0,)
print(type(tuple_with_one_element))
<class 'tuple'>
その理由はPythonのドキュメントより、以下のように記述されています。
なお、タプルを作るのはカンマであり、丸括弧ではありません。
引用元:組み込み型 — Python 3.10.4 ドキュメント(2022/04/27)
余談ですが、以下のような記述(tuple()
+リスト型を入力)も可能です。上述したOK例:要素が一つのタプルの初期化
と同じ振る舞いになります。こちらはカンマ(,
)は不要ですね。
tuple_with_one_element = tuple([0])
print(type(tuple_with_one_element))
一つの条件式でA以上B未満の表現が可能
Pythonでは一つの条件式で例えばA以上B未満の表現が可能です。同様に超過、以下も指定が可能です。例えば、ある変数n
(50)が0以上100未満であることを判定したい場合、以下のような記述が可能です。
n = 50
if 0 <= n < 100:
print("OK")
OK
以下のような数学の表現をそのまま使える点で好んでいます。
0 \leqq n < 100
ちなみに、以下の対義語が以上なのに対して、未満の対義語は超過です。ただし「B未満」に対して「A超過」とは言わない気がするので使うとしたら「A超」でしょうか。「Aより大きい」が無難な気がしました🤔