序
まず、理解しやすくするため、コメントと処理をprint文として追加して、出力により流れをわかりやすくしたのが下記のコードです。
import os
import pandas as pd
path = 'AAA'
pathlist = []
for curDir, dirs, files in os.walk(path):
print(pathlist)
leaf = curDir.split('/')
pathlist.append(leaf)
for a_dir in dirs:
pathlist.append(leaf + [a_dir])
for a_file in files:
pathlist.append(leaf + [a_file])
print()
# 作った2次元リストをデータフレームにする
print("[Phase1-1]作った2次元リストをデータフレームにする")
path_df = pd.DataFrame(pathlist)
print(" path_df = pd.DataFrame(pathlist)")
print(" -> path_df:\n",path_df)
print()
# 重複削除
path_df = path_df.drop_duplicates()
print("[Phase1-2]重複削除")
print(" path_df = path_df.drop_duplicates()")
print(" -> path_df:\n",path_df)
print()
# ソート
path_df = path_df.sort_values(path_df.columns.tolist(),
na_position='first').reset_index(drop=True)
print("[Phase1-3](ソート)の完了後の")
print(" -> path_df:\n",path_df)
print()
print("path_df.columns[-1]= ",path_df.columns[-1])
print()
# 最深部だけ残す
print("[Phase2]最深部だけ残す")
print("[Phase2-1]")
print(" ----check----")
print("path_df.isnull():\n",path_df.isnull())
print(" -------------\n")
bottom_path_df = path_df.isnull().diff(-1, axis=1)
print(" bottom_path_df = path_df.isnull().diff(-1, axis=1)")
print(" -> bottom_path_df:\n",bottom_path_df)
print()
bottom_path_df[path_df.columns[-1]] = ~path_df.isnull()[path_df.columns[-1]]
print("[Phase2-2] (完了後)")
print(" bottom_path_df[path_df.columns[-1]] = ~path_df.isnull()[path_df.columns[-1]]")
print(" -> bottom_path_df:\n",bottom_path_df)
print()
bottom_path_df = path_df[bottom_path_df]
print("[Phase2-3] (完了後)")
print(" bottom_path_df = path_df[bottom_path_df]")
print(" -> bottom_path_df:\n",bottom_path_df)
print()
上記を実行し、出力結果をみながら、下記の説明を読んでみてください。
(読む前のポイントとして
path_df.columns[-1]
は、path_df
の一番最後(-1)の列(column)のインデックスを示します。
-1の意味は、リストでいうa[-1]
と同じです。
この例では、path_dfは0、1、2の3列なので、path_df.columns[-1]
は最後の列である「2」という数になります。
これ以降path_df.columns[-1]
という長ったらしい部分が出てきても「2」という数に置き換えればよいので、多少読みやすくなりますね)
逸れました。本題を続けていきましょう。
[Phase1-3]
[Phase1-3](ソート)の完了後、
path_dfは下記のようになっています。
[Phase1-3](ソート)の完了後の
path_df
0 1 2
0 AAA None None
1 AAA BBB None
2 AAA BBB test2.xls
3 AAA CCC None
4 AAA CCC test3.xls
5 AAA CCC test4.xls
6 AAA test1.xls None
これ以降の 「Phase2(最深部だけ残す)」の最終目的は、
上記path_df
のデータのうち**「各pathの末端(葉)だけを残し、それ以外はNaN
に変換する」**ことです。
この最終目的を頭の片隅に押さえておいてください。
続けましょう。
[Phase2-1]
bottom_path_df = path_df.isnull().diff(-1, axis=1)
ここはちょっと難しいので、右側の一部path_df.is_null()
の内容を先に覗いてみましょう。
path_df.is_null()
もまたデータフレームを表します。
これを表示すると、
----check----
path_df.isnull():
0 1 2
0 False True True
1 False False True
2 False False False
3 False False True
4 False False False
5 False False False
6 False False True
-------------
となっています。path_dfの各データについて、NoneならばTrue, NoneでないならばFalseに置き換えられているだけです。これは簡単ですね。
次、
.diff(-1, axis=1)
これがややこしいのですが、diff(...)
は、隣り合うデータの差分を抽出する関数です。
たとえば
df:
a b c
0 1 1 1
1 2 4 8
2 3 9 27
3 4 16 64
4 5 25 125
に対してprint(df.diff(2))
とすると、2行前 のデータとの差をデータフレームとして返します。
df.diff(2):
a b c
0 NaN NaN NaN #0行目、1行目は、存在しない-2行目、-1行目との差を計算することになるため、Nanになる。
1 NaN NaN NaN
2 2.0 8.0 26.0
3 2.0 12.0 56.0
4 2.0 16.0 98.0
そしてaxis=1
とすると「行の比較」ではなく「列の比較」になります。
したがって.diff(-1, axis=1)
とした場合「各データについて、1列 後ろ のデータとの差」が抽出されます。(-1=負の数なので、前ではなく後ろ のデータとの差分になります)
ここで、pythonではFalseは0、Trueは1、0以外の数はTrueとして扱われるため
True(1) - True(1) = False(0)
True(1) - False(0) = True(1)
False(0) - True(1) = True(-1)
False(0) - False(0) = False(0)
が成立します。
したがって、
path_df.isnull():
0 1 2
0 False True True
1 False False True
2 False False False
3 False False True
4 False False False
5 False False False
6 False False True
これに対して、
.diff(-1, axis=1)
を適用すると、
0列目=0列目-1列目、
1列目=1列目-2列目... と演算されるため、
path_df.isnull().diff(-1, axis=1):
0 1 2
0 True False NaN
1 False True NaN
2 False False NaN
3 False True NaN
4 False False NaN
5 False False NaN
6 False True NaN
となります。(2列目は、存在しない3列目との引き算を行うことになるためNaNになっている)
これがPhase2-1終了後のbottom_path_df
の中身です。
※このPhase2-1と2−2がこのプログラムの一番の鍵です。
ここでやってるのは、「列方向にTrueが連続しているセルをFalseにする」という処理です。
画像処理でいうところの「エッジ検出処理」に近いです。
[Phase2-2]
[Phase2-2]は、欠損値NaNとなってしまった最後列(例では2列目)を回復する処理です。
[Phase2-2]
bottom_path_df[path_df.columns[-1]] = ~path_df.isnull()[path_df.columns[-1]]
path_df.columns[-1]は冒頭で触れたように単なる数(「2」)なので、このコードは
bottom_path_df[2] = ~path_df.isnull()[2]
と書き換えられます。ちょっとスッキリしましたね。
では、右の~path_df.isnull()[2]
の説明に移ります。
「~」は、「ビット反転(NOT)」演算子といい、データのビットを反転させるものです。
簡単に言うと、TrueはFalseに、FalseはTrueに変換されます。
したがって、~path_df.isnull()
は、path_df.isnull()
のTrue,Falseを反転させたデータフレームを表します。
そして、~path_df.isnull()[2]
は、「反転されたpath_df.isnull()
の一番最後の列」を取得していることになります。
まとめると、
bottom_path_df[2] = ~path_df.isnull()[2]
は、 bottom_path_dfの最後の列(NaN)を、「path_df.isnull()を反転したテーブルの最後列データ」に置き換えている、という意味になります。
[Phase2-2] (完了後)
bottom_path_df[path_df.columns[-1]] = ~path_df.isnull()[path_df.columns[-1]]
-> bottom_path_df:
0 1 2
0 True False False
1 False True False
2 False False True
3 False True False
4 False False True
5 False False True
6 False True False
[Phase2-3]
[Phase2-3]は最終目的である、「各pathの末端(葉)だけを残し、それ以外はNaN
に変換する」処理になります。
[Phase2-3]
bottom_path_df = path_df[bottom_path_df]
データのインデックスにFalseを指定するとそのデータはNaNになります。
path_df
は 元コードの最初の方でwalk()関数で得たディレクトリ構造の各データを保持しています.
このpath_df
に対して、True/Falseで構成されたbottom_path_df
をインデックスとして適用することで、
bottom_path_df
がTrueとなっているデータは生き残り、FalseとなっているセルはNaNとなります。
[Phase2-3](完了後)
bottom_path_df = path_df[bottom_path_df]
-> bottom_path_df:
0 1 2
0 AAA NaN NaN
1 NaN BBB NaN
2 NaN NaN test2.xls
3 NaN CCC NaN
4 NaN NaN test3.xls
5 NaN NaN test4.xls
6 NaN test1.xls NaN
以上により、bottom_path_df
には、末端(葉)のデータだけ存在するようになりました。
参考:
https://note.nkmk.me/python-pandas-diff-pct-change/
https://qiita.com/0NE_shoT_/items/8db6d909e8b48adcb203