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