データの単位がファイルであるか、フォルダであるか、によって扱いが異なります。
ファイルの場合は、直接比較しなくても、チェックサム (例えばMD5) を比較することでチェックできます (MD5が偶然一致する確率は無視しています)。単に重複するファイルがあるかどうか、だけで良ければ、以下の1行でチェックできます:
find -type f | xargs md5sum | sort | uniq -cd -w 32 |
重複するファイルがあれば、
3 5c9597f3c8245907ea71a89d9d39d08e ./abc.txt |
のようになります。./abc.txtのMD5が5c9597f3c8245907ea71a89d9d39d08eであり、そのMD5を持つファイルが合計3個あることがわかります。しかし、これでは代表として./abc.txtだけしか表示されません。重複ファイルの完全なリストを得るには、以下のようにします:
find -type f | xargs md5sum > sums.txt |
cat sums.txt | sort | uniq -cd -w 32 | awk '{print $2}' | xargs -n 1 -I{} grep {} sums.txt | sort |
結果は以下のようになります:
5c9597f3c8245907ea71a89d9d39d08e ./abc.txt |
5c9597f3c8245907ea71a89d9d39d08e ./abc-copy.txt |
5c9597f3c8245907ea71a89d9d39d08e ./abc-copy2.txt |
しかし、この方法だと一旦全てのファイルのMD5を計算することになり、時間が掛かります。そこで、チェックサムを比較したのと同じ要領で「同じファイルサイズのファイルが複数個あるファイル」だけMD5を計算するのが良さそうです:
find -type f | xargs wc -c > sizes.txt |
cat sizes.txt | awk '{print $1}' | sort | uniq -cd | awk '{print $2}' | xargs -n 1 -I{} grep "^ *{} " sizes.txt | awk '{print $2}' | xargs md5sum > sums.txt |
全く同じ内容のフォルダを除去するmove_duplicated_folders.shを作成しました:
find . -type d | cut -c 3- > folders.txt |
cat folders.txt | xargs -n 1 basename | sort | uniq -d | xargs -n 1 -I{} grep "{}" folders.txt > folders_with_identical_names.txt |
mkdir -p duplicated_items |
N=0 |
cat folders_with_identical_names.txt | while read LINE |
do |
if [ $N -eq 0 ] |
then |
SRC="$LINE" |
N=1 |
else |
RESULT=`diff -r --brief $SRC $LINE | wc -l` |
if [ $RESULT -eq 0 ] |
then |
echo "$SRC == $LINE" |
else |
echo "$SRC != $LINE" |
fi |
N=0 |
fi |
done > duplicated_items/list.txt |
grep == duplicated_items/list.txt |
grep != duplicated_items/list.txt |
grep == duplicated_items/list.txt | cut -d = -f 3 | while read P |
do |
P2=`echo $P | sed -e 's#/#-#'` |
echo "$P -> duplicated_items/$P2" |
mv "$P" "duplicated_items/$P2" |
done |
これを用いることで、同一内容のフォルダがあった場合、一方をduplicated_itemsの中に移動します。こちらは実際の業務で用いたもので、同名のフォルダ同士のみ比較し、「同名で同一内容」と「同名だが異なる内容」をチェックする仕様になっています。
欠けのチェックについても、bashでcut/sed/awk/dirname/basename/paste/xargs/prinfなどで行うことも可能ですが、Python + Pandasを使った方が楽なので、そのようにします。以下のライブラリを利用します:
import glob |
from pathlib import Path |
import numpy as np |
import pandas as pd |
処理対象は、前々回の記事で利用した
data_000/20101121-090928_a_138275156280588515576727326950 |
data_000/20101121-090928_b_138275156280588515576727326950 |
data_000/20101121-090928_g_138275156280588515576727326950 |
data_000/20101121-090928_n_138275156280588515576727326950 |
(略) |
data_001/20160202-184511_ax_681738386619089223387821743816 |
data_001/20160202-184511_g_681738386619089223387821743816 |
data_001/20160513-112921_ac_469145420997159190987004614138 |
data_001/20160513-112921_ax_469145420997159190987004614138 |
(略) |
data_002/20170525-054622_ax_947488923267857733102498243262 |
data_002/20170525-054622_b_947488923267857733102498243262 |
data_002/20180709-170442_a_984879919960470666771475692433 |
という形式のツリーとします(「YYYYMMDD-HHMMSS_無線LANタイプ_何らかの管理番号」という命名規則)。まずは、処理対象の一覧を取得して、PandasのDataFrameに変換します:
paths = glob.glob('data_???/*') |
df = pd.DataFrame(paths, columns=['path']) |
続いて、YYYYMMDD、HHMMSS、無線LANタイプを取り出して、列として追加します:
df['stem'] = [Path(p).stem for p in paths] |
df['date'], tmp = zip(*[s.split('-') for s in df['stem']]) |
df['time'], df['type'], _ = zip(*[t.split('_') for t in tmp]) |
これにより、多彩なソートや集計が可能になります。なお、「このような処理をいきなりリスト内包表記で記述するのはハードルが高い」という場合は、
dic = {} |
for p in paths: |
s = Path(p).stem |
dic['stem'] = s |
dic['date'], tmp = s.split('-') |
dic['time'], dic['type'], _ = tmp.split('_') |
print(dic) |
break |
のように「練習」するのがオススメです。
ソートや集計の例としては、
df = df.sort_values(['date', 'time', 'type']) |
pd.crosstab([df['date']], [df['type']]) # 日付と無線LANタイプのクロス集計 |
などが考えられます。また、yearとmonthの列を追加することで、
df['year' ] = [d[ : 4] for d in df['date']] |
df['month'] = [d[4 : 6] for d in df['date']] |
pd.crosstab([df['year']], [df['month']]) |
month | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 11 | 12 |
year | |||||||||||
2010 | 7 | 5 | 8 | 4 | 3 | 7 | 5 | 7 | 7 | 4 | 7 |
2011 | 6 | 3 | 8 | 7 | 2 | 1 | 9 | 8 | 1 | 7 | 7 |
2012 | 4 | 5 | 3 | 9 | 5 | 5 | 3 | 4 | 5 | 3 | 6 |
2013 | 0 | 2 | 8 | 4 | 4 | 3 | 6 | 9 | 2 | 6 | 6 |
2014 | 8 | 3 | 4 | 4 | 6 | 3 | 3 | 8 | 5 | 6 | 7 |
2015 | 2 | 4 | 3 | 6 | 8 | 7 | 4 | 3 | 5 | 1 | 4 |
2016 | 6 | 2 | 2 | 5 | 8 | 2 | 5 | 2 | 5 | 7 | 1 |
2017 | 1 | 7 | 4 | 2 | 3 | 5 | 8 | 0 | 2 | 5 | 1 |
2018 | 1 | 5 | 4 | 9 | 9 | 3 | 2 | 6 | 1 | 7 | 3 |
2019 | 3 | 9 | 4 | 7 | 2 | 7 | 8 | 2 | 8 | 8 | 3 |
2020 | 5 | 2 | 4 | 8 | 8 | 1 | 8 | 6 | 6 | 7 | 7 |
のように年と月のクロス集計を行うことも可能です。このようにすることで、2013年1月と2017年8月のデータが無いことが見て取れます。
ただし、この方法ですと、表が大きい場合は目視チェックするのが大変です。以下のようにして、データが無い年と月の組み合わせを一覧表示することも出来ます:
df['year-month'] = ['-'.join([d[: 4], d[4 : 6]]) for d in df['date']] |
set(['%d-%02d' % (y, m) for y in range(2010, 2021) for m in range(1, 13)]) - set(df['year-month']) |
これは、2010-01から2020-12までの文字列のリストを生成し、集合として比較を行うことで実現しています。このような「リスト同士の総当たりの組を作る処理」は、たびたび使うので、
def cross_sets(a, b): |
return [(a_resp, b_resp) for a_resp in a for b_resp in b] |
という形でルーチン化しています。これを使えば、先程の処理は
set(['%d-%02d' % c for c in cross_sets(range(2010, 2021), range(1, 13))]) - set(df['year-month']) |
と記述できます。
独立した通し番号など、クロス集計する必要が無く、単に連続性 (欠番が無いか) だけチェックすれば良い場合もあります。例えば、
data/data-001/preprocessed/224/data_0000-000000.csv |
data/data-001/preprocessed/224/data_0000-000001.csv |
data/data-001/preprocessed/224/data_0000-000002.csv |
data/data-001/preprocessed/224/data_0001-000003.csv |
data/data-001/preprocessed/224/data_0001-000004.csv |
data/data-001/preprocessed/224/data_0001-000005.csv |
data/data-001/preprocessed/224/data_0001-000006.csv |
data/data-001/preprocessed/224/data_0001-000008.csv |
data/data-002/preprocessed/224/data_0002-000010.csv |
data/data-002/preprocessed/224/data_0002-000011.csv |
data/data-002/preprocessed/224/data_0002-000012.csv |
data/data-002/preprocessed/224/data_0003-000013.csv |
data/data-003/preprocessed/224/data_0004-000014.csv |
data/data-003/preprocessed/224/data_0004-000015.csv |
data/data-003/preprocessed/224/data_0004-000016.csv |
data/data-003/preprocessed/224/data_0004-000017.csv |
data/data-003/preprocessed/224/data_0005-000018.csv |
data/data-003/preprocessed/224/data_0005-000019.csv |
data/data-003/preprocessed/224/data_0005-000020.csv |
のような場合、
def extract_digits(strings):
NOT_DIGITS = '[^0-9]+'
DIGITS = '[0-9]+'
DIGITS_SELECT = '(' + DIGITS + ')'
lengths = []
digitss = []
for i in range(100):
digits = []
query = '^'
for j in range(i):
query += NOT_DIGITS + DIGITS
query += NOT_DIGITS + DIGITS_SELECT + NOT_DIGITS + '.*'
#print(query)
replaced = [re.sub(query, '\\1', s) for s in strings]
if replaced == strings:
break
lengths.append(len(replaced))
digitss.append(([int(d) for d in replaced]))
patterns = [re.sub(DIGITS, '###', s) for s in strings]
return patterns, digitss, lengths
patterns, digitss, lengths = extract_digits(strings)
patterns = list(set(patterns))
if 1 < len(patterns) or 1 < len(set(lengths)):
print('multiple patterns')
pattern = patterns[0]
for digits in digitss:
digits = na(sorted(set(digits)))
w = np.where(1 < np.diff(digits))
cont = sorted(set([digits[0]] + list(digits[w]) + [digits[-1]]))
#print(cont)
pattern = pattern.replace('###', str(cont), 1) # 強調しない場合
#pattern = pattern.replace('###', '\033[7m\033[31m' + str(cont) + '\033[0m', 1) # 色を付けて強調する場合
print(pattern)
で、それぞれの番号について連番になっているかどうか、確認できます。実行結果は
data/data-[1, 3]/preprocessed/[224]/data_[0, 5]-[0, 6, 8, 20].csv |
となり、path中の数値は順に、
- 1~3 (欠番なし)
- 224のみ
- 0~5 (欠番なし)
- 0~20 (6と8の後ろに欠番あり)
となっていることがわかります。
パターンが複数ある場合は、multiple patternsと表示されます。path中にtrainとtestが含まれるというようなことが予めわかっている場合は、extract_digits()の呼び出しの直後に
patterns = [p |
.replace('/train/', '/[train, test]/') |
.replace('/test/', '/[train, test]/') |
for p in patterns] |
のように置換をいれておくことで、パターンを一つに纏めることが出来ます。
今回は、重複データを取り除いたり、欠けをチェックする方法について紹介しました。次回は、このPython + Pandasの欠けチェックを発展させたデータセットの管理について紹介したいと思います。
digitssという変数名が登場しましたが、これは私のオリジナルルールで「複数形の複数形」、つまり、2次元配列であることを示しています。このように次元が増えるごとに変数名の末尾にsを追加していくと
for valuess in vaulesss: # 3次元配列に対するループ |
for values in vauless: # 2次元配列に対するループ |
for value in vaules: # 1次元配列に対するループ |
または
for i, valuess in enumerate(vaulesss): # 3次元配列に対するループ |
for j, values in enumerate(vauless): # 2次元配列に対するループ |
for k, value in enumerate(vaules): # 1次元配列に対するループ |
のように、多重ループにしたときに、sが一つずつ少なくなるようになります。あまりsの数が多くなると数えるのが大変ですが。