ある日の悩み・・・
データマインニングの講座を受講していて、csvファイルの扱いが必須なのですが・・・こんな感じでまあ中身が汚いのです。
データ資料がもともと「.csv.pdf」ファイルで変換する必要があったという点が原因かもしれませんが・・・。
この画像を見てわかるように、
- データレコードが文字列としてまとめられている(column名も同様)
- データ間の区切りスペース数が行ごとに異なる
- ところどころ1列目に該当する数値の左に空白が紛れ込んでいる
といった悲惨なデータ構造になっていることがわかります。
当たり前ですが、こういったcsvファイルをいちいち手直しするわけにもいきません。
そこで、手っ取り早くPythonで自動的に整頓してみることにしました。
CSV整頓プログラム(align_csv.py)
CSVを整頓してくれるalign_csv.py
を作成する。
考え方として、ターミナルからpython align_csv.py -i "./csv.file"
で整頓したい
csvファイルを指定・整頓を行えるようにする。
まず使用するモジュールと、コマンドライン(ターミナル)から受けた引数を扱えるようにする。
import pandas as pd
import numpy as n
#コマンドライン(ターミナル)から入力された引数を扱えるよう、sysモジュールをインポート
import sys
#空白や余分な記号を置き換えるため、reモジュールをインポート
import re
#unicodeで表示
pd.set_option('display.unicode.east_asian_width', True)
"""
sys.argvによりターミナルでの入力を受け取る
[0]->実行する.pyファイル(ここでは、align_csv.py)
[1]->第1引数
[2]->第二引数
"""
csv_link = sys.argv[1] #第1引数で、指定した.csvファイルへのリンクを取得。
次にデータフレームのcolumns名(headers)とその値の集合(rows)を整頓するメソッドの作成。
ちなみに下記コードを見て、
「どうしてそんなstrやintに変換してるの?」
「columnsとその値はndarray型として保持されるんだから、astype()
やasarray()
を使って
その配列にある要素を変換しないの?」
という疑問を持たれる方もいるかもしれない。
しかしここで問題となったデータフレームの各行は、ndarray型で、なおかつ要素は空白を含む一つの文字列という扱いになっている。
例:['46 54 87 67'] (type=ndarray)
そのため、astype()等のndarray型の変換メソッドで数値に変換しようにも、要素が空白を含有している一つの文字列であるため適用できず、
split()やre.sub()などで配列内の要素を整形しようにも、ndarray型であるため適用できないことがわかる。
故に、下記のようなコードの形式となっている。
(もっと良い書き方があればぜひ教えていただきたいです・・・)
#column名を整えるメソッド
def align_headers(df):
"""
複数のスペースを、一つのスペースへと置き換える
df.columns.values->dfのcolumns一覧。ndarrayオブジェクトになっている。
re.sub()で扱えるように一度strに変換している。
"""
headers = re.sub(r"\s+", " ", str(df.columns.values))
"""正規表現で余分な記号"[", "]", "'"を削除。
そして残しておいたスペースをsplitでコンマに変換"""
headers = re.sub(r"[\[\]']+", "", headers).split(" ")
return headers
#dfのvaluesを整理するメソッド
def align_rows(df):
new_rows = []
#受け取ったdfを一度ndarrayに変換。
rows=np.array(df)
#末尾に余分な記号が要素として混入しているため削除
rows = np.delete(rows, len(rows)-1, axis=0)
# print(rows, type(rows), len(rows))
for r in rows:
#strに変換。以下3行はalign_headers()と同じ。
str_r = str(r)
str_r = re.sub(r"\s+", " ", str_r)
str_r = re.sub(r"[\[\]']+", "", str_r).split(" ")
#split()では配列の各要素がstrとして出力される。
#こちらでは数値として扱う必要があるため、list(map(int, val))を用いてintへ変換。
int_r = list(map(int, str_r))
new_rows.append(int_r)
# print(r, len(r))
# print(new_rows, len(new_rows), type(new_rows))
return new_rows
最後に.csvファイルの読み込み・書き込みと各メソッドの実行を行うメソッドの作成。
def align_csv(df_link):
""".csvファイルの読み込み。
ついでにlambda関数を適用してlstrip()でデータフレームに含まれている
改行コード(/x0c)を除去する"""
df = pd.read_csv(df_link).apply(lambda d: d.str.lstrip())
headers = np.array(align_headers(df))
rows = np.array(align_rows(df))
"""整列させた新しいデータフレームを作成"""
new_df = pd.DataFrame(rows, columns=headers)
"""元となった.csvファイルへの書き込み。
to_csvでは、indexに何も指定していないと新たなindexが追加されてしまい、列数が一つ増えてしまう。
そのため、index=Falseにして、indexを追加しないようにする"""
new_df.to_csv(df_link, header=True, index=False)
#変換後の.csvファイルの確認
df_read = pd.read_csv(df_link)
print(df_read)
align_csv(csv_link)
実行してみると・・・
$ python align_csv.py "./terrible_data.csv"
#変換前 ->
kokugo shakai sugaku rika
0 30 43 51 63
1 39 21 49 56
2 29 30 23 57
3 95 87 77 100
4 70 71 78 67
.. ...
162 0 8 2 9
163
45 26 29 24
164 73 31 43 32
165 60 85 89 80
166
#変換後 ->
kokugo shakai sugaku rika
0 30 43 51 63
1 39 21 49 56
2 29 30 23 57
3 95 87 77 100
4 70 71 78 67
.. ... ... ... ...
161 82 78 80 88
162 0 8 2 9
163 45 26 29 24
164 73 31 43 32
165 60 85 89 80
以上
ここまでご覧いただきありがとうございました。
レコードに欠損値が含まれている場合、整頓工程を条件ごとで分岐させたい場合など、
まだまだ改善の余地はありますがひとまず使えるものになりました。
まだ慣れていない部分もあるため、もっと効率のいい書き方などあれば教えていただけると幸いです。