この記事では、Pythonのデータ分析用ライブラリPandasについて、頻出のプロパティ・関数・メソッドをまとめていきます。
どの場面で、どんな使い方をするか?をメインに置きながら、Pandasへの理解を深めるためのTipsもできる限り詰めこんでみました。
ぜひPandasの「ハンドブック」として使ってください。
また、Python自体が不安な人は、独学にオススメの教材をまとめておいたので、参考にしてみてください。
AI時代の到来を受けて、大人気プログラミング言語となった「Python」。 ただ興味を持ってみたものの『何から始めたらいいのかわからない。』 という人は意外と多いです。 そこでこの記事では、非情報系Python[…]
※今回の記事では、お馴染みのTitanicデータをKaggle Notebook上で操作していきます。
まずは実際にハンズオンで確認するために、CSVファイルを読み込んでいきましょう。
#ライブラリのインポート
import pandas as pd
import numpy as np
# csvファイルの読み込み
train_df = pd.read_csv("../input/titanic/train.csv")
test_df = pd.read_csv("../input/titanic/test.csv")
train_dfを実行すれば、trainデータのテーブルを確認できます。(test_dfならtestデータ)
train_df
Titanicデータは、各行が「乗客一人ひとり」を表し。
各列が「乗客の情報」を表します。
では準備完了です。
始めていきましょう。
※ 目次を開いて好きなところに飛んでください。
3つのPandasオブジェクト
Pandas完全初心者もいるかもしれないので、まず初めにPandasに登場する3つのオブジェクトを軽く紹介していきます。
すでに理解できている方は飛ばしてください。
またここで説明する内容を深堀したい方は、以下の書籍をオススメしておきます。
Seriesオブジェクト
Seriesオブジェクトは、インデクス付きデータで構成される一次元配列です。
インデクスはPythonリストのインデクスと同じ。あるいはPython辞書の「キー」とみることもできます。
実際SeriesオブジェクトはPython辞書の要素が縦に並んだようなものだとみることができます。
# 文字列で列名を指定
# 列名はどれを選んでもいい
train_df["Fare"]
上のコードを実行すると、次のような結果が返ってきます。
左列が「インデクス(キー)」、右列が「値」です。
type()関数で確認すると、これがSeriesオブジェクトであることが確認できます。
type(train_df["Fare"])
# 出力:pandas.core.series.Series
繰り返しますがSeriesオブジェクトは一次元配列です。
配列の形状情報が入ったshapeプロパティを呼び出してみましょう。
train_df["Fare"].shape
# 出力:(891,)
次項のDataFrameオブジェクトを見ればわかりますが、二次元配列だった場合shapeプロパティで返ってくるのは(行数, 列数)という値です。
しかしSeriesオブジェクトのshapeプロパティは、(要素数, )しか表示されません。
これはSeriesオブジェクトが一次元配列であることを意味します。
DataFrameオブジェクト
DaraFrameオブジェクトもSeriesオブジェクトと同様に、インデクス付きデータで構成されます。
Seriesオブジェクトに名前(列名)を付けて、横方向に結合した構造しています。
データ読み込みの時と同様、train_dfを実行してください。
train_df
これがDataFrameオブジェクトです。
それぞれの列が、名前(ラベル)つきのSeriesオブジェクトから成り立っています。
type()関数で確認すると、これがDataFrameオブジェクトであることが確認できますね。
type(train_df)
# 出力:pandas.core.frame.DataFrame
DataFrameオブジェクトは二次元配列なので、shapeプロパティで(行数, 列数)という値が返ってきます。
train_df.shape
# 出力:(891, 12)
ここで気づいた人もいるかもしれませんが、インデックスは列数に含まれません。
Python辞書で「キー」を単独の要素としてカウントしないのと同じ感覚です。
DataFrameオブジェクトは「キー:値1, 値2,…」といった構造のPython辞書を縦に並べたものだと考えればいいです。
Indexオブジェクト
Indexオブジェクトは、インデクスを格納した一次元配列です。
それだけ覚えておけば十分。
詳しく知りたい方は、上で紹介した書籍を参考にしてください。
DataFrameオブジェクトのcolumnsプロパティを呼び出すと、列名が格納されたIndexオブジェクトが返ってきます。
train_df.columns
# 出力:Index(['PassengerId', 'Survived', 'Pclass', 'Name', 'Sex', 'Age', 'SibSp',
'Parch', 'Ticket', 'Fare', 'Cabin', 'Embarked'],
dtype='object')
type()関数で確認すると、これがIndexオブジェクトであることが確認できます。
type(train_df.columns)
# 出力:pandas.core.indexes.base.Index
shapeプロパティを呼び出せば、Indexオブジェクトが一次元配列であることがわかります。
train_df.columns.shape
# 出力:(12,)
テーブルの形状を把握する
テーブルが何行のレコードと、何列のカラムから成り立っているか。
つまりデータ数(行数)と、変数の個数(列数)を見るための方法をまとめます。
行列の形を知る:shapeプロパティ
train_df.shape
# 出力:(891, 12)
shapeプロパティを呼び出せば、(レコード数, カラム数)が返ってきます。
長さを出す:len関数
print("レコード数:", len(train_df))
print("カラム数:", len(train_df.columns))
# レコード数:891
# カラム数:12
レコード数とカラム数を個別に出力したいときは、len関数が使えます。
DataFrameをそのままlen関数に入れた場合、レコード数(行数)がカウントされるので注意。
カラム数を出したいときは、columnsでIndexオブジェクトを呼び出してその要素数を数えましょう。
データを確認する
続いてテーブル内のデータを確認するプロパティ・メソッドまとめ。
データ型・欠損値の早見表:info()メソッド
#info()メソッドでデータ型と欠損値の数をまとめて確認
train_df.info()
info()メソッドを使えばカラム別に非欠損値数とデータ型が確認できます。
出力は次のようになります。
インデクス・カラム名・非欠損値数・データ型の順で並んでいますね。
データ型の確認:dtypesプロパティ
train_df.dtypes
データ型のみを確認したいときは、dtypesプロパティが使えます。
出力はインデクス=カラム名、値=データ型のSeriesオブジェクトです。
object型に識別されるのは、カテゴリ変数orテキストデータ。
ただし数値型(int64 or float64)と識別されていても、数値データとは限らないので注意です。
例えば、日曜日=1, 月曜日=2・・、みたいに曜日に数字を割り振っていても数値型と識別されます。
実際はカテゴリ型です。
欠損値数:isnull()メソッド+sum()メソッド
train_df.isnull().sum()
isnull()メソッドとsum()メソッドを組み合わせると、欠損値の数を確認できます。
train_df.isnull()
を実行すると、欠損値か否か?bool値で表したDataFrameが返ってきます。
ここでsum()メソッドを使うと、列ごとに集計されます。
欠損値=True= 1(False=0)なので、列ごとの欠損値の数がかえってくるわけです。
ちなみにFalseが返ってきても、欠損値の場合もあるので注意。
99999や空白スペース(“”)が入っていたりします。
要約統計量:describe()メソッド
train_df.describe()
カラムごとのデータ分布を知りたいときは、describe()メソッドが使えます。
各水準(カラム)ごとの要約統計量が返ってきます。数値データの列だけ集計されます。
describe(exclude = 'number').T
カテゴリ変数を集計したいときは、引数にexclude=’number’を指定してください。
そのままだと表が見にくい(と僕は思う。)ので、.Tで転置を取る(行列を入れ替える)のがいいでしょう。
カテゴリのユニーク値:unique()メソッド
train_df["Embarked"].unique()
# 出力:array(['S', 'C', 'Q', nan], dtype=object)
具体的なカテゴリ値を知りたいときに使います。
Seriesオブジェクトにしか使えません。返り値はNumpy配列のndarrayです。
カテゴリのユニーク数:nunique()メソッド
train_df["Embarked"].nunique()
# 出力:3
何種類のカテゴリ値からなるか、を調べるために使えます。
カテゴリごとの個数:value_counts()メソッド
train_df["Embarked"].value_counts()
カテゴリ値の分布を知りたい場合に使えます。
データを抽出する
続いてDataFrameからの所望のデータ抽出する方法をまとめていきます。
Pandasはスライスがわかりにくいので、その部分は丁寧に解説していきます。
複数列を抽出
train_df[["Pclass", "Fare"]]
DataFrameから複数カラムを抽出したいときは、カラム名のリストを渡せばOK。
同じくDataFrameが返ってきます。
ちなみにリストで渡せば、カラム名が一つでもDataFrameが返ってきます。
train_df[["Fare"]]
条件を満たすレコードを抽出:query()メソッド
train_df.query("20 <= Age <= 40 and Survived == 1")
特定の条件を満たすレコードのみを抽出したいときは、query()メソッドを使ってください。
文字列で条件式を指定すると、それを満たすレコードがDataFrameで返ってきます。
データのスライス⓵:loc
train_df.loc[0:10,"PassengerId":"Name"]
# train_df.loc[:10, :"Name"]でもOK
locを使うと、Pandasスタイルのインデクスを使ってスライスができます。
Pandasスタイルのインデクスとは「左のインデクス」と「カラム名(列ラベル)」のことです。
引数は[行, 列]の順で指定してください。
リストでインデクスを指定すると、任意の行と列をスライスできます。
train_df.loc[[0, 3, 5, 7, 9], ["Survived", "Pclass", "Name"]]
また行の指定には条件式を入れることも可能です。
条件式を満たすレコードのみがスライスされます。
# Ageが20歳以上の行を抽出
train_df.loc[train_df.Age >= 20, ["Survived", "Pclass", "Name"]]
データのスライス⓶: iloc
train_df.iloc[0:10, 0:3]
ilocを使うと、Pythonスタイルのインデクスを使ってスライスを行います。
Pythonスタイルのインデクスとは、Pythonリストを作成したときに振られている、見えない数値インデクスのこと。
このようにPandasオブジェクトには、locで指定する「明示的インデクス」と、Pythonスタイルの「見えないインデクス」の両方が振られています。
locとの使い方の違いは次の二つ。
- 行・列ともに数値で指定する
- 最後の数値は指定範囲に含まれない(0:5とすると, 0~4が返ってくる)
不要なデータを削除:drop()メソッド
train_df.drop(["Name", "Ticket", "Cabin"], axis=1, inplace=False)
不要なデータを削除することでも、データを抽出は可能です。
そんな時は、drop()メソッドが使えます。
この例では、カラム(列)を削除していますが、レコード(行)も削除できます。
使い方は
- 削除したい行のインデクス or 列のカラム名を、リスト形式で指定
- 行(レコード)を削除するならaxis=0, 列(カラム)を削除するならaxis=1
- inplace=Falseなら値が返ってくる(非破壊), inplace=Trueなら元データを書き換え(破壊的)
といった感じです。
ただdrop()メソッドにはもう一つ別の記述方法があります。
train_df.drop(columns =["Name", "Ticket", "Cabin"], inplace=False)
行を指定したいときはcolumns ⇒ indexに変更すればOK。
こっちのほうが、簡潔かつ直感的なので個人的には好きです。
欠損値を含むレコードを削除:dropna()メソッド
train_df.dropna(axis = 0, subset=["Cabin"], inplace=False)
dropna()メソッドを使えば、欠損値を含むレコードを削除することができます。
使い方は
- axis=1にすればカラム削除もできる。デフォルトはaxis=0。
- subsetに指定したカラム中での欠損値の有無が判定される。省略すれば全カラムが対象。
- inplace=Falseなら値が返ってくる(非破壊), inplace=Trueなら元データを書き換え(破壊的)
という感じ。
ただしdropna()はNoneを欠損値としてみなさないので注意が必要です。
欠損値にNoneが含まれる場合は、replace()メソッドでNaNに置き換えましょう。
train_df.replace("None", np.nan, inplace=True)
インデックスを作り直す:reset_index()メソッド
train_df.dropna(subset=["Cabin"], inplace=False).reset_index(drop=False)
reset_index()メソッドを使えば、データ抽出でまばらになったインデックスを振りなおすことができます。
drop=Falseにしているので、もともとのインデクスが「Index」というカラム名で残っていますが、drop = Trueにすれば消えます。
データを集約する
続いては与えられたデータを属性ごとに集約するgroupby()メソッドの使い方をいくつか紹介。
Groupbyオブジェクト
train_df.groupby("Embarked")
# 出力:<pandas.core.groupby.generic.DataFrameGroupBy object at 0x7efef3dc3990>
groupby()メソッドを使うと、「指定したカラム内に含まれるカテゴリ」ごとにレコードをグループ分けしたGroupbyオブジェクトが返ってきます。
文字で読むとよくわからないかもしれませんが
train_df.groupby("Embarked")
は
train_df.query("Embarked == 'S'")
train_df.query("Embarked == 'C'")
train_df.query("Embarked == 'Q'")
を内部で同時に行っていると考えてください。
カテゴリ変数ごとの集約値を出す
train_df.groupby("Embarked")[["Age"]].mean().reset_index()
Groupbyオブジェクトに対して「どのカラムの値」に「どんな処理」をするのかを指定すれば、カテゴリ変数ごとのの集約値が出せます。
- 「Ageのカラムの値」の「平均値」を算出
- [[“Age”]]とすることでDataFrameが返ってくる。[“Age”]ならSeries
- reset_index()で新しいインデクスをふった。なければEmbarkedがインデクス。
複数の集約値を同時に出す:agg()メソッド
train_df.groupby("Embarked").agg({'Age': 'mean', 'Fare' : 'max'})
Groupbyオブジェクトに対して、「複数のカラム」に「異なる処理」を行いたいときには、agg()メソッドを使います。
引数には{‘カラム名1’ : ‘処理関数1’, ‘カラム名2’ : ‘処理関数2’}のように辞書を指定します。
また「一つのカラム」に「複数の処理」を実行することもできます。
やり方は関数をリストで指定するだけです。
train_df.groupby("Embarked").agg({'Age': ['max', 'min', 'mean', 'median']})
データを結合する
続いてテーブル間のデータ結合を行うためのメソッドを紹介。
その前にまず、この後の説明で使うテーブルを作成しておきます。
次のコードを実行してください。
# Pclass(部屋の等級)ごとの、Fare(料金)平均値をまとめてテーブルをFare_meanに格納
Fare_mean = train_df.groupby("Pclass")[["Fare"]].mean().reset_index()
# カラム名を変更
Fare_mean.columns = ["Pclass", "Fare_mean"]
Fare_meanを実行して、次のようなテーブルができていればOK。
Fare_mean
テーブル同士の結合:concat()メソッド
pd.concat([train_df, test_df], axis=0, join="outer", ignore_index=True)
concat()メソッドを使えば、複数のテーブルを一つに結合できます。
891レコードのtrainデータと、418レコードのtestデータが行方向に結合され、1309レコードのテーブルになっていますね。
使い方は
- 結合したいオブジェクトのリストを渡す。3つ以上結合することもできる。
- axis=0で縦方向(行)、axis=1で横方向(列)の結合。デフォルトはaxis=0。
- ignore_index=Trueでインデクスを振りなおす。reset_index(drop=True)メソッドとおなじ。
joinに関しては、join=”outer”を指定すると「外部結合」、join=”inner”で「内部結合」となります。
外部結合:テーブルを丸ごとそのまま結合。足りない部分はNaN値が入る。
内部結合:列が共通しているときのみ結合。非共通部分は削除される。
となり、上の例の場合join=”inner”を指定するとSurvivedのカラムが消去されます。
pd.concat([train_df, test_df], axis=0, join="inner",ignore_index=True)
ついでに、結合したテーブルをもとに戻すコードも載せておきます。
# 結合したテーブルをall_dfに格納
all_df = pd.concat([train_df, test_df], axis=0, join="outer", ignore_index=True)
# all_dfからtrain_dfとtest_dfを抽出しなおす
train_df = all_df.iloc[:len(train_df), :]
test_df = all_df.iloc[len(train_df):, :].reset_index(drop=True)
test_dfのほうはreset_index(drop=True)でインデクスを振りなおすのを忘れないでください。
他テーブルからの値の紐づけ:merge()メソッド
外部のテーブルから、操作テーブルに何かしらの値を紐づけたい場合もあるでしょう。
そのような時には、merge()メソッドが使えます。
Excelに詳しい人は、VLOOKUP関数と似たものだといえばわかるでしょうか。
pd.merge(train_df, Fare_mean, on="Pclass", how="left")
先ほど作成したFare_meanとtrain_dfを、共通カラムPclassを「キー」として紐づけてみます。
一番右にFare_meanという列が追加されているのがわかります。
使い方は
- 主テーブル, 外部テーブルを指定。
- on=”Pclass”でPclassを「キー」として指定。Pclassを基準として紐づけ。
- howは結合様式を指定。concatのjoinと同じ。left, right, outer, innerが選べる。
という感じです。
ただし「キー」の名前は両テーブルで同じである必要はありません。
ためしにFare_meanのPclassをRoom_classに変更してみます。
Fare_mean.columns = ["Room_class", "Fare_mean"]
Fare_mean
これをtrain_dfと結合します。つぎのコードを実行してください。
pd.merge(train_df, Fare_mean, left_on="Pclass", right_on="Room_class", how="left")
さっきと全くおなじテーブル出来上がります。
日時型データに対する処理
つづいて日時・日付データの処理に関するメソッドをまとめます。
ただTitanicデータにはないので、テキトーに作成しながら説明していきます。
日時型・日付型への変換:to_datetime()メソッド
pd.to_datetime()を使うことで、日時を表した文字列の列データを列の dtype がdatetime64[ns]型、各要素が Timestamp型である列データに変換することができます。
実際に作ってみましょう。次のコードを実行してください。
pd.to_datetime(["1th of December, 2022",
"2022-Dec-2",
"12-03-2022",
"20221204"])
# 出力:DatetimeIndex(['2022-12-01', '2022-12-02', '2022-12-03', '2022-12-04'], dtype='datetime64[ns]', freq=None)
とこんな感じで、文字列なら割となんでもありです。
このようにto_datetime()メソッドの基本的な使い方は、文字列型をdatetime64[ns]型に変えること。
次の予約テーブルで実際によく行われる処理を確かめてみます。
次のコードを実行して、reserve_datetime列の値のデータ型を見てみましょう。
datetime_data["reserve_datetime"].dtype
# 出力:dtype('O')
するとデータ型がObject型つまり文字列であることがわかります。
ここで
datetime_data["reserve_datetime"] = pd.to_datetime(datetime_data["reserve_datetime"])
と引数にSeries丸ごと指定するとdatetime型変えてくれます。
確認してみましょう。
datetime_data["reserve_datetime"].dtype
# 出力:dtype('<M8[ns]')
datetime64[ns]型になっていますね。
ただ日時型のデータがUNIX時間として保存されていることもあります。
UNIX時間とは「1970年1月1日午前0時0分0秒からの経過秒数」で時間を表したものです。
例えば、このテーブルのtsカラムがそうです。
UNIX時間もto_datetime()メソッドで、datetime型に変換できます。
コードはこんな感じ。
train_df["date"] = pd.to_datetime(train_df["ts"], unit="ms")
引数は先ほどと同じ、Seriesを丸ごと渡せばいいです。
ただUNIX時間には「どこまで細かく時間を取るか」形式が三つあって
UNIX桁数 | 時間範囲 | unit |
10桁 | 秒[s] | s |
13桁 | ミリ秒[ms] | ms |
16桁 | ナノ秒[ns] | ns |
という感じでunit指定を変える必要があります。
日時要素の取り出し
datetime64[ns]型のデータからは、年/ 月/ 日/ 時刻/ 分/ 秒/ 曜日が好きに取り出せます。
上で使用した予約テーブルを使って、それぞれのコードを見てみます。
ついでに、再び文字列に戻すコードも載せておきます。
# 年を取得
datetime_data["reserve_datetime"].dt.year
# 月を取得
datetime_data["reserve_datetime"].dt.month
# 日を取得
datetime_data["reserve_datetime"].dt.day
# 曜日(0=日曜日, 1=月曜日)を数値で取得
datetime_data["reserve_datetime"].dt.dayofweek
# 時刻の時を取得
datetime_data["reserve_datetime"].dt.hour
# 時刻の分を取得
datetime_data["reserve_datetime"].dt.minute
# 時刻の分を取得
datetime_data["reserve_datetime"].dt.second
# 指定したフォーマットの文字列に変換
datetime_data["reserve_datetime"].dt.strftime("%Y-%m-%d %H:%M:%S")
一応ポイントは
- datetime.dt.~の形で統一。
- 返ってくるのはSeries。
です。
補足(随時追加)
これも載せたほうがいいなーって感じたものをちょこちょこ補足に追加していきます。
「いやいや、これは必要でしょ?」というご意見・ご要望お待ちしております。
任意の関数を適用:apply()メソッド
DataFrame.apply
任意のカラムを基準に並べ替え:sort_values()メソッド
imp.sort_values(“imp”, ascending=False, ignore_index=True)
データ型を変更する:astype()メソッド
参考文献