本記事では学習のアウトプットとして、『Python実践データ分析100本ノック』に書かれている各ノックのコードのうち、難解と思われる部分の意味を解説していきます。「本に書かれている解説だけでは理解が難しい(;一_一)」と感じた方、この記事を読んで理解の一助となれば幸いです。
Python実戦データ分析100本ノックは、データの集計や分析のためのpandasや、グラフ描画に使用するmatplotlib、機械学習を行うためのscikit-learnなど、データ分析に欠かせない要素を実際に自分で手を動かしながら学ぶことができる本です。
今回は第3回として、ノック11-15をやっていきます。
追記:現在第二版が2022年6月に出版されています。
本記事は第1版の内容の解説です。
第二版の解説記事もいずれ書かせていただこうと思っていますので今しばしお待ちください!
第2章 小売店のデータでデータ加工を行うノック10本
この章では、大まかな流れは第1章と同じで、複数のデータを読み込み、結合した後で集計することです。しかし、第1章から異なるのは、読み込んだデータ内に表記の揺れがあることです。表計算ソフトを使用していても、例えば、名前を記入する時、人によって半角スペースを使う人もいれば、全角スペースを使う人もいるのではないでしょうか?指定されていても誤ってしまう場合もありますよね。そんな人的ミスを自動的に修正していきます。
ノック11:データを読み込んでみよう
#これまでと同様の方法でuriageのcsvファイルを読み込みます
import pandas as pd
uriage_data = pd.read_csv("uriage.csv")
uriage_data.head()
ノック12:データの揺れを見てみよう
uriage_data["item_name"].head()
売り上げデータの、item_name列を確認すると、商品A,商品a,商品 A など同じ商品でも書式が異なるものが混在しているのが分かります。人間なら、簡単にできることが機械やAIにとって難しい時があるんです(モラベックのパラドックス)。この場合でいうと、表記に揺れがありますが、「あ、多分同じ商品だな」って推測できますよね。機械やAIだと高度な計算は人間より得意でも、こういった感覚的なことが苦手なようです。
uriage_data["item_price"].head()
売り上げデータの、item_price列を抽出すると、欠損値NaNがあることが分かります。こちらは単純にデータが欠けている状態です。集計対象に空のデータがあると不都合な場合がありますので、なんとか処理しておきたいと感じますね。
ノック13:データに揺れがあるまま集計しよう
#ノック8と同様にto_datetimeで、purchase_dateを日時を表す文字列datetime(日付と時刻)に変換します。
uriage_data["purchase_date"] = pd.to_datetime(uriage_data["purchase_date"])
#ノック8と同様にdt.strftimeで引数に指定した書式文字列の形式にdatetimeを変換します。
#%Y:西暦年を表示。%m:月を表示。
#そして、purchase_monthという新たな列に形式を変えた後のpurchase_dateを格納しています。
uriage_data["purchase_month"] = uriage_data["purchase_date"].dt.strftime("%Y%m")
#ノック9と同様にpandasのpivot_tableメソッドを用いてデータ集計を行います。
#引数の中で指定するindex=,columns=,aggfunc=はノック9で解説させていただいたものと同様ですね。
#fill_value=:これはvalue(値)を"="で指定した値でfill(埋める)ことができます。
#今回は0を指定したので、欠損値があれば自動的に0で埋めてくれます。
res = uriage_data.pivot_table(index="purchase_month", columns="item_name", aggfunc="size", fill_value=0)
res
purchase_dateを20YY(年)m(月)という形式に変換した後、欠損値を0で埋めることができました。
ノック14:商品名の揺れを補正しよう
#strは文字列型といって大文字・小文字を変換したり、大文字・小文字かどうかを判定したりできます。
#str.upper()はすべての文字を大文字に変換できます。
uriage_data["item_name"] = uriage_data["item_name"].str.upper()
#str.replace()は引数に指定した文字列を同じく引数に指定した文字列に変換できます。
#今回は、"全角スペース"と"スペース無し"となっているで、全角スペース→スペース無しへの変換となります。
uriage_data[“item_name”] = uriage_data[“item_name”].str.replace(“ ”, “”)
#今回は、""の中に"半角スペース"と"スペースなし"となっているので、半角スペース→スペース無しへの変換となります。
uriage_data[“item_name”] = uriage_data[“item_name”].str.replace(" ", “”)
#uriage_dataを要素(今回は商品名)でソートします。引数にascending=Trueと記入しているので、昇順に並べます。
uriage_data.sort_values(by=[“item_name”], ascending=True)
商品名の揺れを補正するために全ての文字を大文字に変換(例.商品a→商品A)し、全角スペースと半角スペースを消すことができました(例.商品 A→商品A)。
ノック15:金額欠損値の補完をしよう
#ノック7で登場したisnull()を使用して、指定した要素ごとに欠損値かどうかを判定しています。
#any()は行または列とごとにTrue(今回はnull)が一つでもあればTrueと判定するメソッドです。
#つまり、isnull().any()で行または列ごとに欠損値を一つでも含んでいるかを判定します。
#axis=0であれば列(縦方向),axis=1であれば行(横方向)の判定をそれぞれ指定できます。
uriage_data.isnull().any(axis=0)
#isnull()を用いて、item_price列のうち欠損値が含まれるものをflg_is_nullとして新しい変数に格納しています。
flg_is_null = uriage_data["item_price"].isnull()
#for in でループを形成します。
#list()はリストを作成することができます。
#リストはミュータブル(変更可能)なデータ構造を持ち、要素の挿入や削除を自由に行うことができます。
#loc.[]は行と列のラベルを指定して1つの要素または範囲を指定して参照することができます。
#条件式のようなイメージですね。
#.unique()を用いると要素の固有値つまり、重複しないように要素を抜き出します。
#つまり、uriage_dataのflg_is_null列の固有値のみをリスト化してtrgという新しい変数に入れます。
for trg in list(uriage_data.loc[flg_is_null, "item_name"].unique()):
#~はFalsetを表します。
#.max()で最大値を取り出すことができます。
#条件が複雑なので、外側から詳細に解読していきます。
#まずuriage_data.loc[,"item_price"].max()ですが、
#loc関数で何らかの条件に沿った行の"item_price"列の最大値を取得します。
#次に(~flg_is_null)&(uriage_data["item_name"] == trg)ですが、
#「flg_is_nullの中にいない」かつ「uriage_dataの“item_name”列のなかでとtrgの中にも存在する値、
#つまり、欠損値があるitem_name」を満たすかを判定しています。
#構文の解釈として、欠損値がある商品名と同じ商品名の金額データを取得してpriceの中に格納しています。
price = uriage_data.loc[(~flg_is_null) & (uriage_data["item_name"] == trg), "item_price"].max()
#uriage_dataのitem_price列に対して、loc関数で欠損を起こしているデータを指定して、そこに1つ上の行で取得したpriceを格納しています。
uriage_data["item_price"].loc[(flg_is_null) & (uriage_data["item_name"]==trg)] = price
uriage_data.head()
#先ほどの長い条件で金額の欠損値をしっかり埋めることができているかを確認します。
#unique()で重複なくすべてのitem_nameを取り出し、
#sort_values()で固有値をアルファベット順で並べたデータをリスト化してtrgに格納しています。
for trg in list(uriage_data["item_name"].sort_values().unique()):
#ここでは、loc関数でuriage_dataのitem_name列からtrgの中にあるものと等しいもの、
#つまりuriage_dataからもitem_nameの固有値を参照します。
#uriage_dataにはitem_price列も存在するため、同じ行のitem_price列のデータの最大値を取得しています。
#ざっくりいうと、trgの中にある商品名にと同じ名前の商品名をuriage_dataからも探して金額を取り出そうとしているイメージです。
print(trg + "の最大額:" + str(uriage_data.loc[uriage_data["item_name"]==trg]["item_price"].max())
#こちらは最大値ではなく、最小値を取り出そうとしていますが、ロジック自体は同じですね。
#skipna=Falseは、NaN(欠損値)をskip(無視)するかを指定します。
#今回はFalseなので、無視せず、NaNがあれば、そのままNaNと表示します。
+ "の最小額:" + str(uriage_data.loc[uriage_data["item_name"]==trg]["item_price"].min(skipna=False)))
金額の欠損値を探し出し、プログラムで自動的に補完してから商品すべての欠損値が埋まるっていることをprintメソッドで表示させることができました。
ここまでで、ノック11-15は完了です。お疲れ様でした。いきなりloc関数が出てきたと思ったら、中身の条件式がこれまたいきなり複雑で、解読が難しいですよね(;一_一)頭の中でどんな事が起きているかをゆっくりでいいので考えていきましょう。そのロジカル思考こそがプログラマーっぽいですよね(`・ω・´)次回は2章後半に入ります。引き続きよろしくお願いいたします(^^)/。