本講義では、pandasを使用したデータの読み込み、整形・加工、書き込みの実践テクニックをご紹介します。
pandasの最新バージョンは、2022/8/19リリースのver 1.5.1です。
本講義で使用するファイルを、下のコードを実行してダウンロードしておきましょう。
! git clone https://github.com/tendo-sms/python_intermediate_2022
%cd python_intermediate_2022/03_pandas
配列データを扱うパッケージ・モジュールにはPandasだけではなく、csvモジュール、NumPyも有名です。これらは、どのように使い分けるべきでしょうか。
csvファイルの様々な読み込み方法をご紹介しながら、これらをどのように使い分けるべきかをご紹介します。
本講座の例題では、入力ファイルのサンプルとして「sample.csv」と「sample2.csv」の2つのファイルを使用します。
sample.csvの内容は、次のとおりです。
measureID | date | operator | temperature | measureValue | measureUnit |
---|---|---|---|---|---|
MEA001 | 2022/11/1 | suzuki | 25 | 1000 | sec |
MEA001 | 2022/11/1 | suzuki | 25 | 1000 | sec |
2022/11/2 | yamada | 20 | 999 | sec | |
MEA002 | 2022/11/4 | sato | room | 98 | min |
MEA004 | 3-Nov-22 | adam, smith | 18 | 9 | hour |
MEA005 | 15 | 8 | hour | ||
MEA003 | 11-05-2022 | yamada | (欠測) |
sample2.csvの内容は、次のとおりです。sample.csvと異なる点は、赤字にしています。
measureID | date | operator | temperature | measureValue | measureUnit |
---|---|---|---|---|---|
MEA001 | 2022/11/1 | suzuki | 25 | 100 | sec |
MEA001 | 2022/11/1 | suzuki | 25 | 100 | sec |
2022/11/2 | yamada | 20 | 99 | sec | |
MEA002 | 2022/11/4 | sato | room | 98 | min |
MEA004 | 2022/11/3 | adam | 18 | 97 | hour |
MEA005 | 18 | 97 | hour | ||
MEA003 | 11-05-2022 | yamada | (欠測) |
python標準で用意されている最も身近なモジュールはcsvモジュールです。
import csv
csvObj = []
for rowItems in csv.reader(open("sample.csv", encoding="utf_8")):
csvObj.append(rowItems)
csvObj # リストのリストとして読み込んだ
値にカンマが含まれている「adam, smith」も、きちんと一つの項目として読み込んでくれます。
こういった「CSV書式なら当たり前に処理すべきこと」を自分で書かずに済みます。
以下の例ではcsv.DictReaderを使用しています。ファイル内の先頭行を項目名として使用し、各行の項目を辞書形式で返してくれるものです。「何列目が何の情報か」を辞書のキーとして管理してくれる、構造化の第一歩と言えるでしょう。
import csv
csvObj = []
for rowItems in csv.DictReader(open("sample.csv", encoding="utf_8")):
csvObj.append(rowItems)
csvObj # 辞書のリストとして読み込んだ
csvモジュールは、あくまで「csvファイルをリストに入れる」ということだけができます。気軽に使うことができる反面、機能は少ないです。
行列計算や、データ構造化では必須となるデータの整形・加工などを行いたい場合は、NumPyやPandasなどを使うことになります。
numpyは行列計算ライブラリなので、CSVを含む各種テキストファイルから行列データまたはその類似データをnumpy.ndarrayとして読み込むことができます。扱うデータが本当に条件を満たせるか、良く確認してください。
ただし、以下は引数の設定で対応可能です。
import numpy as np
csvObj = np.loadtxt("sample2.csv", encoding="utf_8", delimiter=",", dtype=str)
csvObj # np.ndarrayとして読み込んだ
成形処理など、より複雑な処理が必要な場合はgenfromtxt関数を使用できます。
ただし、CSV読み込み時に整形処理も全て行おうとすると煩雑な引数設定になりがちなので、無理に読み込み時に処理するのではなく後段に整形処理を設けることを検討しましょう。
このように、今回のサンプルデータはNumPyでも読み込むことができます。しかし、やはりNumPyは「行列計算」ライブラリです。
データ構造化ではデータの整形・加工や表計算がメインであり、そのような操作はPandasの方が得意です。
という使い分けを覚えてください。
複雑なテーブル形式の整形や表計算などはpandasで処理するのが簡単なので、ファイルの読み込み自体もpandasで行うのがオススメです。numpy.genfromtxt相当以上の引数も整備されています。 デフォルト設定では以下のようなデータとして読み込まれます。
import pandas as pd
csvObj = pd.read_csv("sample.csv", encoding="utf_8")
csvObj # pd.DataFrameとして読み込んだ
もう少し複雑な整形も関数の引数で行った例を示します。多くの機能がありますが、それら機能を覚える必要はありません。調べ方を覚えてください。(helpを用いる、インターネットで公式ドキュメントを見る等)
import pandas as pd
csvObj = pd.read_csv(
"sample.csv", encoding="utf_8",
parse_dates=["date"],
converters={"temperature": lambda e:25 if e=="room" else e},
na_values={"measureValue":["(欠測)"]}
)
csvObj # pd.DataFrameとして読み込んだ
それでもなお、temperature列の最終行の空白文字がNaNにならなかったことに注意してください。
こういった特殊値の扱いは、読み込み後の整形処理で置き換えるのがオススメです。
ここからは、pandasでCSVファイルを読み込んだデータを整形・加工していく例を示します。
読み込み時の整形は最低限とした状態から例示していくので、どういった書き方ならソースコードが読みやすいのか、境界線をどこに置くのが良いのか考えながら、見ていきましょう。
ここで目指すのは以下とします。
import pandas as pd
csvObjOrg = pd.read_csv("sample.csv", encoding="utf_8")
csvObjOrg
単純にファイルを読み込んだだけでは処理されない箇所は以下の2箇所です。
まずはこの2つを処理しないと、列内の型の統一ができません。これらを置き換えるにはreplaceメソッドを使う方法とloc/ilocメソッドを使う方法があります。
csvObj1 = csvObjOrg.replace("room", 25)
csvObj1
ここでのポイントは以下の通りです。
データの整形は列単位での処理も多いので、列や要素の参照・抽出と代入の方法を見ていきましょう
# at, iat : 要素の参照
print(csvObjOrg.at[2, "date"]) # 行インデックスオブジェクト、列の名前で指定する
print("="*30)
print(csvObjOrg.iat[2, 1]) # 行インデックス値、列インデックス値で指定する
# loc, iloc : 単一要素ではない範囲指定の参照
print(csvObjOrg.loc[:, "date"]) # 行インデックスオブジェクト、列の名前で指定する。1次元データなのでSeries型が返ってくる
print("="*30)
print(csvObjOrg.loc[0:1, "date":]) # 両方を範囲指定するとDataFrame型が返る
print("="*30)
print(csvObjOrg.loc[1]) # 2次インデックスを指定しないと全範囲扱い
# print(csvObjOrg.loc[1, :]) # これと同様の扱いとなる
print("="*30)
print(csvObjOrg.loc[1, "date"]) # 範囲指定を全く使用しないとatと同じものが返ってくる
# print(csvObjOrg.at[1, "date"]) # これと同じものを取得している
atやlocよりも頻繁に使用されるのは、以下の2つで紹介する行・列の抽出方法です。
# その他、特定列のみの抽出 :
print(csvObjOrg["measureValue"]) # 必要な列だけを選択
print("="*30)
print(csvObjOrg.measureValue) # 列名を直接書く事も可能だが列名に空白含まない等の条件を守る必要があるため、あまり一般的ではない
# その他、特定行のみの抽出:
print(csvObjOrg[[True, False, True, False, True, False, True]]) # 行数と同じ長さのbool配列を渡すと、Trueが指定された箇所の行のみを抽出。直接bool配列を直書きする機会は恐らくないですが・・・
print("="*30)
print(csvObjOrg["measureUnit"]=="sec") # measureunit列の各要素の値が"sec"かどうかをbool型リスト(正しくはSeriesオブジェクト)でこのように取得できるので・・・
print("="*30)
print(csvObjOrg[csvObjOrg["measureUnit"]=="sec"]) # 単位がsecとなっているデータのみを抽出したDataFrameをこのように求められます!
# print(csvObjOrg.index >= 2) # 通常列だけでなく、後述の行インデックスに対してもbool型リストを取得できます
簡単に書けて便利な反面、以下のような落とし穴もあるので注意してください。
不要な行を除外した部分的なデータを作成できたら、後はそれを新たなオブジェクトとして変数に代入しておきましょう!
csvObj2 = csvObjOrg[csvObjOrg["measureUnit"]=="sec"]
csvObj2
その他にも、pandasには行や列除外の重要なメソッドがあります。
例えば、measureIDが振られていない計測は無視したいとします。その場合はDataFrame.dropnaメソッドを使用しましょう。
といった機能を持っています。
csvObj3 = csvObjOrg.dropna(
axis="index", # 0または"index"なら行を削除、1または"columns"なら列を削除
how="all", # 削除条件の指定。"all"または"any"から選択
subset="measureID" # 無効値のチェック対象となる列名を指定。複数列しらべる場合はリストで渡せる
)
csvObj3
元ファイルの内容は、ゼロ行目と1行目が全く同じ内容が重複して書かれてしまっていました。こうした重複を削除するにはDataFrame.drop_duplicatesメソッドが便利です。
csvObj4 = csvObj3.drop_duplicates(
keep="first", # デフォルトも"first"なので通常は設定不要
# subset="measureID" # 例えば「ID重複している時点で無効なデータとみなしたい」等
)
csvObj4
空欄の項目には、実は2種類の意味がありそうですね。
これらのうち、記述省略している場合に対応するためには「欠損部を何かで埋める」という処理が必要です。
# csvObj5 = csvObj4.fillna("(unknown)") # どの列の空欄も一律で同じ値で埋めたい場合
# csvObj5 = csvObj4.fillna({"operator": "(unknown)", "temperature": "room"}) # 列ごとに埋めるべき値が異なる場合
#csvObj5 = csvObj4.fillna(method="ffill") # 1行前の内容をコピーして埋めたい場合. dateとoperator以外の空欄も埋められてしまう
csvObj5 = csvObj4.copy()
csvObj5.loc[:, ("date","operator")] = csvObj5[["date","operator"]].fillna(method="ffill")
csvObj5
列の選択は前述のように、インデックスとして列名を与える方法でも実現できますが、DataFrame.reindexメソッドを使用する方法もあります。
※ 同じ処理をするための方法が複数用意されているのがpandasの痛し痒しな部分です。それぞれの方法で少しずつ挙動も異なるため、いたずらに複数の方法を混在させると可読性低下や思わぬ不具合の元になります。
csvObj6 = csvObj5.reindex(
columns=["measureID", "date", "measureValue", "measureUnit", "temperature"], # 欲しい列の名前を欲しい並びで記述
# index=[3, 5, 4] # 行に対しても同様の記述が可能
)
csvObj6
ここまでで重複や無効データを除外でき、measureIDを各行固有なものにできました。しかし行のインデックスは歯抜けになった「0, 3, 4, 5」となってしまっています。そこで、measureIDを行インデックスとして設定してみましょう。ついでに、0からの連番インデックスを振り直すDataFrame.reset_indexもご紹介します。
csvObj6.reset_index( # 行インデックスをゼロからの連番に振り直す
drop=False # 振り直し前のインデックスを通常列として残さない場合はTrue
)
csvObj7 = csvObj6.set_index(
"measureID",
drop=True # インデックス設定したい列を通常列としても残したい場合はFalse
)
csvObj7
行インデックスに名前が付いたことで、前述のat/locとiat/ilocの違いが分かりやすくなりました!
print(csvObj7.at["MEA002", "temperature"])
print(csvObj7.iat[3, 1])
もしも、reset_indexする前のcsvObj5に対してiloc/iatを使ったら、何が返ってくると思いますか?
print(csvObj6.loc[3, :])
print("="*30)
print(csvObj6.iloc[3, :])
列の追加は簡単です。列に名前を付けて、リストやタプル、Seriesオブジェクト等、「長さが行数と同じリスト様オブジェクト」を代入します。
csvObj7a = csvObj7.copy()
csvObj7a["isMin"] = [False,True,False,False,False] # 配列を直接渡す例
csvObj7a
csvObj7a["isSec"] = csvObj7a["measureUnit"]=="sec" # Seriesオブジェクトを渡す例
csvObj7a
csvObj7a["isDay"] = False # 全ての行に同じ値を入れる事も出来ます
csvObj7a
ここまで来れば、特定列のみを対象にした安全な欠損値置換ができるようになりました!
csvObj7b = csvObj7.copy()
csvObj7b["measureValue"] = csvObj7b["measureValue"].replace({"(欠測)":np.nan})
csvObj7b["temperature"] = csvObj7b["temperature"].replace({"room":25})
csvObj7b
ここでmeasureValueは一見、数値に見えますが現状ではまだ文字列のままです。これは今回、CSVファイルではmeasureValue列に「(欠測)」という文字列が含まれていたために列の型を数値と認識できなかったからです。数値のみの記述だと保証できない時は型変換の整形が必要かもしれない事を念頭に置きましょう。
csvObj7b.iat[1,1]
print(csvObj7b.dtypes)
print("="*30)
csvObj7b["measureValue"] = csvObj7b["measureValue"].astype(float)
csvObj7b["temperature"] = csvObj7b["temperature"].astype(float)
print(csvObj7b.dtypes)
ここまで整形したDataFrameとこれまで見てきた編集方法を使用して、measureValueの単位をsecに統一してみましょう!
csvObj8a = csvObj7b.copy()
csvObj8a["SecValue"] = np.nan
csvObj8a["SecValue"][csvObj8a["measureUnit"]=="sec"] = csvObj8a["measureValue"][csvObj8a["measureUnit"]=="sec"]
csvObj8a["SecValue"][csvObj8a["measureUnit"]=="min"] = csvObj8a["measureValue"][csvObj8a["measureUnit"]=="min"] * 60.0
csvObj8a
SecValueは代入できたものの、なにか警告が出ていますね。恐れずに警告の内容を読んでみましょう! 同じ警告が2回表示されていて、挙げられたURLの先を読んでみると、「行や列のインデックスを使用しての代入をするな。代わりにlocを使え」と書いてあるので、そのような書き方に改めましょう。 ちなみに代入の時だけが問題で読んで参照するときはOKなので、下のように左辺のみ修正すれば大丈夫です。ただ、違いを覚えるよりもlocを必ず使うように覚える方が安心でしょう。
csvObj8b = csvObj7b.copy()
csvObj8b["SecValue"] = np.nan
csvObj8b.loc[csvObj8b["measureUnit"]=="sec", "SecValue"] = csvObj8b["measureValue"][csvObj8b["measureUnit"]=="sec"]
csvObj8b.loc[csvObj8b["measureUnit"]=="min", "SecValue"] = csvObj8b["measureValue"][csvObj8b["measureUnit"]=="min"] * 60.0
csvObj8b.loc[csvObj8b["measureUnit"]=="hour", "SecValue"] = csvObj8b["measureValue"][csvObj8b["measureUnit"]=="hour"] * 3600.0
csvObj8b.loc[csvObj8b["measureUnit"]=="day", "SecValue"] = csvObj8b["measureValue"][csvObj8b["measureUnit"]=="day"] * 86400.0
csvObj8b
DataFrameにはapplyというメソッドがあり、「1行を表すSeriesオブジェクトから何かを計算するユーザ定義関数を各行に適用し、新たな1列に相当するSeriesオブジェクトを返す」という処理をさせることができます。行と列の扱いを逆転させて、各列から新しい1行を作成する事もできますよ。
このapplyメソッドを使用すると、上記の単位変換をもっと分かりやすい形で実装する事ができます。
def tm2sec(srRow):
if srRow["measureUnit"] == "sec":
return srRow["measureValue"]
if srRow["measureUnit"] == "min":
return srRow["measureValue"] * 60.0
if srRow["measureUnit"] == "hour":
return srRow["measureValue"] * 3600.0
if srRow["measureUnit"] == "day":
return srRow["measureValue"] * 86400.0
csvObj8c = csvObj7b.copy()
csvObj8c["SecValue"] = csvObj8c.apply(tm2sec, axis="columns")
csvObj8c
最後に、ここまで整形したDataFrameをCSVファイルに保存しましょう。保存対象とするのは、ここまで整形したDataFrameから不要な列を削除した以下のようなものとします。
csvObj9 = csvObj8c.reindex(columns=["date", "SecValue", "temperature"])
csvObj9
保存自体は非常に簡単、メソッドを呼び出すだけです。ただ、行インデックスと列の名前をCSVファイルに含めるかを考えましょう。特に行インデックスは、今回のようにIDを割り当てた場合は保存したいでしょうが0開始の連番だけの場合は不要という事も多いです。
csvObj9.to_csv(
"result.csv",
encoding="utf_8",
header=True, # 列名を1行目に出力するかどうか
index=True # 行インデックスを1列目に出力するかどうか
)
DataFrameのメソッドは大抵、自分のオブジェクト自体は変更せず、自分のオブジェクトから改変した別のオブジェクトを作成して返します。例えばインデックスを振り直すreset_index等。
from pprint import pprint
csvObj6b = csvObj6.reset_index(drop=True)
pprint(csvObj6b)
print("="*30)
pprint(csvObj6) # csvObj6とcsvObj6bは違うオブジェクトで、csvObj6は変更されていない
これらのメソッドの大半には、inplaceというオプション引数が用意されています。これを使用するとメソッド呼び出し時にオブジェクト自身を書き換えてくれます。
csvObj6c = csvObj6.copy()
pprint(csvObj6c)
print("="*30)
csvObj6c.reset_index(drop=True, inplace=True)
pprint(csvObj6c)