警告を出さない「データ分析100本ノック」の回答【4章】

# 概要

データ分析100本ノックの本を久々に再開しているが、大量の警告が発生するわ副作用だらけのコードなせいでタイプミスしたらまともに動かないわで困っていた。
勉強の出力、ライブラリの習熟、後発の人への支援を目的として自分の回答と解説をしてみる。

どのぐらい警告が出るかっていったらこのレベル。（PandasのAPIを使いこなせていないコードというのもツッコミどころだけど）

![warnings.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395414/f02e9e31-da16-5205-bc8b-d959b2c25560.png)

答えのデータを見たら本人も警告が出ていることがわかっているらしく、こんなことしてた。本にこれに対する言及が無いのはかなり悪質だし、実務でこれやると破滅への第一歩なので、皆はやらないようにね。

```py
# 警告(worning)の非表示化
import warnings
warnings.filterwarnings('ignore')
```

# レギュレーション

- 各ノックの出力の内容を合わせる
  - 変数名・行列名など
- 上のレギュレーションに違反しない限り、副作用は出さない
- 警告は出さない
- 本文の内容に関する解説はしない
  - 買おうね

# ノック32別解

サンプルコードを打ち込んで出た警告は次の通り

```
C:\Users\    \anaconda3\Lib\site-packages\sklearn\cluster\_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  super()._check_params_vs_input(X, default_n_init=10)
C:\Users\    \AppData\Local\Temp\ipykernel_18172\1809050377.py:8: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  customer_clustering["cluster"] = clusters.labels_
```

FutureWarningは「K-Meansのモデルの作成時、n_initのデフォルト数値を10からautoに変更したから、注意してね」の警告と思われる。
SettingWithCopyWarningは他のコードでの警告の出方をみると「メソッドチェーンで繋ぐ形等でデータを加工しないと副作用が出てデータが壊れる可能性があるから注意してね」という意味に見える。
今回は新しい列を追加するときに警告が出ていて、これはassignで新しい列を追加する形にすれば元のインスタンスは触らないため、例外が出なくなるらしい。

つまりこうやってマイナーチェンジするだけで警告は抑制できる。処理自体には不審点はないからここで止めれば大丈夫だろう。
なお、Jupyter Notebookはコードの実行順をある程度自由に変更できる。このコードブロックみたいにコードブロック外の変数を触ってしまうと前のコードをもう一回実行しようとしたときに動作が変わってしまう。
そういうこともあるから同じ名前の変数に上書きするのはやめようね。

```py
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
sc = StandardScaler()
customer_clustering_sc = sc.fit_transform(customer_clustering)

kmeans = KMeans(n_clusters=4, random_state=0, n_init='auto')
clusters = kmeans.fit(customer_clustering_sc)
customer_clustering = customer_clustering.assign(cluster=clusters.labels_)
print(customer_clustering["cluster"].unique())
customer_clustering.head()
```

# ノック36

冒頭で警告が並んでいるのを見せたのがここのお手本。
コードとその結果をよく見ると、各顧客・各月のデータに対して手動で（PandasのAPIを使わずに）結合させているようだ。
そのせいで値の入っていないセルも量産しているように見える。
なので私はこのように分けてみた。

## 年月テーブルの作成

まずはある月と、そこから6ヶ月遡るテーブルを作成する。
このテーブルと元々の操作対象を結合すれば、for文の処理を最小限に（≒負荷を最低限に抑えて）
結合用のデータを作成することができる。

```py
# 予め6ヶ月スパンになる年月の対応表を作る
year_months = list(uselog_months["年月"].unique())
CHECK_MONTH_SPAN = 6
month_count = len(year_months)
# 「month_i」はそのログからiか月前
month_table = pd.DataFrame([year_months[i:i + CHECK_MONTH_SPAN + 1] for i in range(month_count - CHECK_MONTH_SPAN - 1)],
                           columns=[f"month_{CHECK_MONTH_SPAN - i}" for i in range(CHECK_MONTH_SPAN + 1)])

predict_data = pd.merge(uselog_months, month_table, left_on="年月", right_on="month_0", how="inner")
```

## 過去の統計データの紐づけ

上記のデータはまだ過去の使用頻度のデータが結合されていないから、順番に結合していく。
このとき内部結合をすればそもそも欠損データを含まれるデータが作成されない。
なお、一月まるっと利用されていないときも欠損データになるはずだが、
元々の手本も同じ問題を持っているのと、今回のデータはそういったものも含まれてなさそうなので今回は気にしなくてよさそうだ。

```py
for i in range(6):
    # join用テーブル
    # count_xはx+1か月前なのが仕様のため、結合時に使用するラベルをずらす
    uselog_work = uselog_months[["年月", "customer_id", "count"]]
    predict_data = predict_data.merge(uselog_work, left_on=["customer_id", f"month_{i + 1}"], right_on=["customer_id", "年月"], suffixes=[None, f"_{i}"], how='inner')

# 処理に使った列の削除
predict_data = predict_data.drop(labels=[f"{head}_{tail}" for head in ("年月", "month") for tail in range(CHECK_MONTH_SPAN)],
                                axis=1)\
                            .drop(labels=["usedate", f"month_{CHECK_MONTH_SPAN}"], axis=1)\
                            .rename({"count":"predict"}, axis=1)
predict_data.head()
```

# ノック37

著者のコードを警告の握り潰しの部分を消して実行したら警告が多すぎて出力が途中で打ち切られてた。
**こっそり警告を握り潰して動いたことにするとかメチャクチャ悪質**だと思ってるけど、嘆いても仕方ないので動かせるように修正してみる。
例によって各業の処理をfor文で回してしまっているので、そこも改善対象。

各行に対して処理を行った、新しい列を作るにはDataFrame.apply()のメソッドを使えば良いらしい。
ラムダ式が使えればよかったが一行に収めると可読性が激減するので、関数として切り出して処理を行った。

```py
predict_data["now_date"] = pd.to_datetime(predict_data["年月"], format="%Y%m")
predict_data["start_date"] = pd.to_datetime(predict_data["start_date"])

from dateutil.relativedelta import relativedelta


def to_delta_month(x):
    delta = relativedelta(x.now_date, x.start_date)
    return delta.years * 12 + delta.months


predict_data = predict_data.assign(
    period=predict_data[["now_date", "start_date"]].apply(to_delta_month, axis=1)
)

predict_data.head()
```

# おわりに

4章で感じたのは次の3点です。

- 列や行に対する直接の操作を行うと意図しないシャローコピーが書き換わるからやめようね
- Jupyter Nodebookはセル単位での副作用に注意しよう
- 例外も警告も、握り潰しは「私は能力がありません」と言っているに等しいからやめようね