Goodな生活

INTPの好奇心の受け皿

『Python実践データ分析100本ノック』を2周やってみて調べたこと

「とりあえずPython勉強しないとやばい」という焦りと勢いに任せ、100本ノック(正確には60本ノック)やりました。
第1,2章のデータ加工、第3-5章の機械学習、第10章の言語処理です(最適化と画像認識のところはおいおい)とりあえず2周やったので、調べたことメモします。

どういう風にやったかというと、

  • 説明文を読み、何も見ずにコードを書いてみる(たぶん全体の5%ぐらいしかできず)
  • 説明文を読み、ググったり参考書見ながらやってみる
  • 写経して何やってるか後追いで調べる
  • 写経しても分からないからスキップする(2周目にやる)

って感じです。

一応この本の題名「実践データ分析」にある通り、クラスタリングや決定木といった手法も扱います。しかし理論的なことはほとんど書かれていません。逆に言うと、理論はさておきデータ分析やるなら前処理が大事なんだよということが身をもって分かります。

それではメモです。所要時間はだいたい1章につき1周目は3,4時間、2周目は1時間ぐらいかかりました。

ノック1:カレントディレクトリの指定

まずこれをやらないとデータの読み込みができません。

#カレントディレクトリの指定
%cd C:\\Users
#カレントディレクトリの表示
%pwd

ノック10:データの可視化(.plot)

ピボットテーブルの形でグラフ用のデータを作るまではOK.

#グラフ用データ作成
graph_data = pd.pivot_table(join_data, index='payment_month', columns = 'item_name',values=['price'], aggfunc = 'sum')

ここであえてgraph_dataのインデックスを取得し、リスト化する意味が分からなかった。単純に.plotでオプション指定すればよいのではないか。

#グラフ用描画
graph_data.plot(grid=True, legend = True)

ノック15:Seriesを用いたデータの参照(.loc)

for文の中の.locの使い方が勉強になった。

#売上データのうち、価格が欠損値になっているデータを抽出
flg_is_null = uriage_data['item_price'].isnull()
flg_is_null.head()
0    False
1     True
2     True
3    False
4     True
Name: item_price, dtype: bool

flg_is_nullにはインデックスとFalse/Trueのブール値が含まれる。

#flg_is_null を売上データに渡し、価格が欠損値となっている商品名を抽出
uriage_data.loc[flg_is_null,'item_name']
1       商品S
2       商品A
4       商品A
6       商品A
14      商品A
       ... 
2987    商品K
2990    商品O
2992    商品C
2996    商品Q
2997    商品H
Name: item_name, Length: 387, dtype: object

すると、flg_is_nullでTrueとなったインデックスと同じ行のデータだけ引っ張ってこれる。どうやら作者はこの文法が好みらしい。他にも似たような書き方が出てくる。一旦flg_is_serialというインデックス付きのブール値が格納されたseriesを作成し、 locでこのseriesを参照する。するとTrueの箇所のインデックスだけを拾ってくる、という仕組み。

このflg_is_nullが条件式の意味なんだと理解した(違うかもしれない)。あるデータフレームdfの列column1が「column1 == condition」という条件を満たすときにcolumn2 の列を取り出す場合は、
df.loc[df['column'] == 'condition', 'column']となる。

loc。。。まだ使いこなせない

ノック17:.to_datetime

本来日付型として扱うべきデータが数値型になっているので直す問題。模範解答みたいにまどろっこしいことをせずとも、.to_datetimeを使えばよいのではないか。

kokyaku['登録日'] = pd.to_datetime(kokyaku['登録日'],format='%Y/%m/%d')
kokyaku['登録年月'] = kokyaku['登録日'].dt.strftime("%Y%m")

いずれにせよExcel, csvの段階で直せるならそっちで直した方がよい気がする。

ノック15と同じくここでもlocが大活躍。中に条件式を入れても使える。

fromString = pd.to_datetime(kokyaku_data.loc[flg_is_serial==False,'登録日'])

ノック23:カテゴリーごとの集計(.value_counts)

模範解答のように.groupbyで集計しても問題ない。.value_countsを使ってもカウントや割合を出力できる。

customer_join['class_name'].value_counts()
オールタイム    2045
ナイト       1128
デイタイム     1019
Name: class_name, dtype: int64
customer_join['class_name'].value_counts(normalize = True)
オールタイム    0.487834
ナイト       0.269084
デイタイム     0.243082
Name: class_name, dtype: float64

ノック23:.to_datetimeで使うデータ型

なんで.to_datetimeに文字列を入れるのか分からなかった。

customer_start = customer_join.loc[customer_join['start_date'] > pd.to_datetime("20180401")]

以下のページを参照して、とりあえずデータ型を揃えるものだと理解。
deepage.net

ノック26:条件指定(.where)(.mask)

模範解答だとwhereで条件しているが、maskを使っても可能。等号の向きが変わるのに注意。

#whereを用いる場合
uselog_weekday_max['routine_flg'] = 0
uselog_weekday_max['routine_flg'] = uselog_weekday_max['routine_flg'].where(uselog_weekday_max['count'] < 4,1)
#maskを用いる場合
uselog_weekday_max['routine_flg'] = 0
uselog_weekday_max['routine_flg'] = uselog_weekday_max['routine_flg'].mask(uselog_weekday_max['count'] >= 4,1)

ノック28:日付情報の計算(relativedelta)

relativedeltaを使う以外にも.TimedeltaやDateOffsetというものが使えるらしい。これは後々やろう。
sinhrks.hatenablog.com

ノック32:KMeans(random_state)

random_stateの初期値はランダムに設定される。結果の再現性を確保するため、random_state=0を設定する。

ノック34:PCA(n_components)

n_components = 次元の数、元の特徴量の空間(次元)よりも大きくなることはない。

ノック35:クロス集計(.crosstab)

クラスタリング結果を元に、groupbyで着実に集計した方がミスがない気もするが、クロス集計だけなら簡単に済ませられる。何より結果が見やすい。

#クラスターと退会有無のクロス集計
pd.crosstab(index=customer_clustering['cluster'], columns = customer_join['routine_flg'])
is_deleted	0	1
cluster		
0	821	19
1	1231	18
2	0	771
3	790	542
#クラスターとルーティーン有無のクロス集計
pd.crosstab(index=customer_clustering['cluster'], columns = customer_join['is_deleted'])
routine_flg	0	1
cluster		
0	52	788
1	2	1247
2	499	272
3	226	1106

ノック42:模範解答の誤り(.to_datetime)

たいした誤りではないです(でも解決するまでに2時間ぐらいかかった気がする)。
for文の中でexit_cstomer['exit_date’]のデータ型をdatetimeに変える1行を追加します。

for i in range(len(exit_customer)):
    exit_customer["exit_date"].iloc[i] = exit_customer["end_date"].iloc[i] - relativedelta(months=1)
#この1行を追加↓exit_customer['exit_date']をあらかじめpd型にしておく
exit_customer["exit_date"] = pd.to_datetime(exit_customer["exit_date"])
exit_customer["年月"] = exit_customer["exit_date"].dt.strftime("%Y%m")
uselog["年月"] = uselog["年月"].astype(str)
exit_uselog = pd.merge(uselog, exit_customer, on=["customer_id", "年月"], how="left")
print(len(uselog))
exit_uselog.head()

他の方の方法も試しました。こちらでは何故かうまくいきませんでした。
teratail.com

ノック46:ダミー変数化(.get_dummies)(.factorize)

素直にget_dummies使えばよい。別解としてカテゴリ変数を数値に変換する.factorizeも使える。これはカテゴリ変数が1列だけの場合だと書きやすいのか。いまいちどういう場面で有用か分からないけど参考まで。

ノック94:Mecabのインストール

コードを書くというよりMeCabのインストールが大変だった。
やった作業としては、

  • Pythonの最新版(3.9.6)のダウンロード
  • コマンドプロンプトでpythonを起動できることを確認
  • MeCabのダウンロード(mecab-0.996.exe, mecab-0.996-64.exe)
  • 環境変数の編集(Path, 編集, 新規, インストール先のパスを指定)

公式のMeCabだとうまく起動せず、野良ファイルだとうまくいった。なんでだろう。

ノック94:if文のインデント

ifの後にインデント空白があるとエラーになる。for文の中では for文よりも下げたインデントで始まり、そのレベルのインデントが続く限り、それはfor文がまわす対象範囲として認識される。 一見同じ空白に見えても片方はtab,もう片方はspaceでインデントされたいる場合もエラーになることもある。

pen.envr.tsukuba.ac.jp

ノック97:リストの拡張(.append)(.extend)

  • .append(): 引数に渡された値をリストの末尾に渡す
  • .extend() :引数に渡された別のリストを追加してリストを拡張する

qiita.com

参考書としてこちらの本も使いました。

やることは莫大にある。継続が大事。