1. はじめに
おそらく、Pythonでデータの読込や加工をするときPandasを使うことになると思いますが、ちょっとしたデータ処理にはさほど難しいコーディングが必要なわけではないので、コピペしてちょちょいとコードを変えるだけで動いてくれます。
このため、私はついつい詳しいメソッドやプロパティは勉強せずそのままにしてしまい、何度も調べる羽目になりました。また、同じような実行結果になる方法が何通りもあったりして、どれを使えばよいか迷ったりもしました。
今後研究でも使うかもしれないし、そろそろちゃんと勉強しないといけないなと思い、この度Pandasを一通り勉強してきました。忘備録も兼ねて、簡潔にメソッドやプロパティとその実行結果をまとめてみようと思います。
Pandasの学習にあたっては、キノコードさんのPandas入門動画を参考にしています。
目次
2. Series
Seriesはよく、「ラベル付き一次元配列」と言われます。これにカラム名を付けて横に繋ぎ合わせれば、DataFrameになります。
・Seriesの作り方
まずは二通り紹介します。一つ目は配列を引数に渡す方法、二つ目はnumpyの一次元配列を引数に渡す方法です。
import pandas as pd
series1 = pd.Series([0,1,2,3,4,5])
series1
import numpy as np
series2 = pd.Series(np.array([0,1,2,3,4,5]))
series2
実行結果は以下のように、同じになります。
0 0
1 1
2 2
3 3
4 4
5 5
dtype: int64
特にこだわりがないのであれば、普通の配列を引数に渡すのでもいいと思います。
・SeriesにIndex(ラベル)を振る
SeriesのIndexは、デフォルトではナンバリングされています。このため、通常の配列のようにインデックス指定で個別のデータを取り出すこともできます。
この一方でSeriesは、各データに対して任意のラベルを割り振ることができます。
例えば以下の通りです。
series1.index = ["sora","horii","murakabe","arata","amata","ooki"]
series1
上から順にインデックス名が割り振られていることが分かります。
sora 0
horii 1
murakabe 2
arata 3
amata 4
ooki 5
dtype: int64
各データに対して最初からラベルを割り振っておきたいのであれば、辞書型のデータを引数に渡すか、indexという引数を指定して明示的に渡すこともできます。
まず、辞書型のデータを渡す方法です。
dict_series = pd.Series({"sora":1,"ame":2,"toto":5})
dict_series
次に、index引数に直接渡す方法です。
incorporated_index_series = pd.Series([1,2,3],index=["sora","ane","toto"])
incorporated_index_series
結果は次のようになります。
sora 1
ane 2
toto 5
dtype: int64
・Seriesから特定のデータを取得する
割り振ったindexからデータを取得したり、特定の条件でデータを取得したりします。
series1["sora"]
#複数のデータの取得は、文字列ではなくて配列を渡す形になります。
series1[["sora","ooki"]]
条件にあったデータを取得したいならば、その条件を直接指定してあげれば大丈夫です。
series1[ series1 > 2 ]
実行結果は次のようになります。
arata 3
amata 4
ooki 5
dtype: int64
・その他のプロパティやメソッド
値を取得したいならvaluesプロパティ、配列の長さを求めたいならsizeプロパティにアクセスします。
全体で欠損値があるかどうかを調べるときは、hasnansプロパティ、各indexについてnullがあるかどうかはisnullメソッドを呼び出します。
series3 = pd.Series([70,40,20,None])
series3.hasnans
hasnansはBoolean値を返却します。
上の例でいえば、Noneが配列の中にあり、これは欠損値として扱われるのでTrueです。
series3.isnull()
この実行結果は以下の通りになります。
最後のデータが欠損値なので、そこだけTrueになります。
0 False
1 False
2 False
3 True
dtype: bool
3. DataFrame
・DataFrameの作り方
データフレームの作り方は、配列や辞書型のデータを渡したり、numpyの配列を渡す方法があります。
まず、配列を渡す方法を示します。
df = pd.DataFrame([[0,1,2],[3,4,5],[6,7,8]])
df
二次元配列を渡しただけでIndexやColumnに何ら値を渡していない、ラベリングしていないので、自動的にナンバリングされます。
0 1 2
0 0 1 2
1 3 4 5
2 6 7 8
もし、IndexやColumnをしてしておきたいならば、indexやcolumnの引数を指定して配列を渡すことでラベリングできます。
df = pd.DataFrame([[0,1,2],[3,4,5],[6,7,8]])
df.column = ["c1","c2","c3"]
df.index = ["r1","r2","r3"]
df
DataFrameの引数として始めから指定することもできます。
df = pd.DataFrame([[0,1,2],[3,4,5],[6,7,8]],
columns=["c1","c2","c3"],
index=["r1","r2","r3"])
df
どちらでも以下のような形になります。
c1 c2 c3
r1 0 1 2
r2 3 4 5
r3 6 7 8
次に、辞書型のデータを渡す方法を紹介します。
カラム名をキーに、データをバリューの辞書型のデータとして渡すので、自動的にカラム名は決まります。
インデックス名は、DataFrameに引数として渡す方法や、インスタンス化した後でindexを上書きする方法の二通りあります。ここでは後者でindexを指定しています。
dict_df = pd.DataFrame({
"c1":[0,1,2],
"c2":[3,4,5],
"c3":[6,7,8]
})
dict_df.index = ["r1","r2","r3"]
dict_df
出力結果は配列を渡した時と同じです。
もし、カラム名やインデックス名を変更したくなったら、renameメソッドを呼び出して再代入します。
df = df.rename(index={"r1":"commence"})
df
ちゃんとラベルを変えられました。
c1 c2 c3
commence 0 1 2
r2 3 4 5
r3 6 7 8
一行目のindex名がcommenceだと面倒なので、r1になおしておきます。
・特定の行・列の取り出し
まず、列を取り出す方法から紹介します。
列を取り出すには、単純にカラム名を指定するだけでいいのですが、Seriesとして取り出すか、DataFrameとして取り出すかの違いがあります。
df["c1"]
# Seriesとして取り出し
df[["c1"]]
# DataFrameとして取り出し
複数のカラムを指定したいのであれば、必然的にDataFrameとして取り出すことになると思います。
列を指定したい場合は、locないし、ilocを使います。
locがラベルを指定する方法で、ilocが列番号を指定する方法です。
df.loc["r1"]
df.iloc[1]
この二つは同じ実行結果になります。
c1 3
c2 4
c3 5
Name: r1, dtype: int64
実は、locとilocは、列も指定することができます。
例えば、行としては2から3行目だけを選び、列としては1、2列目だけを選びたいときは、以下のようにします。
df.iloc[1:3,0:2]
合計で2行2列のDataFrameを取り出せました。
c1 c2
r2 3 4
r3 6 7
・そのほかのプロパティやメソッド
データフレームの基本情報を取得できるプロパティやメソッドとして、dtypesやshape、sizeなどがあります。dtypesは各列ごとのデータ型を表示し、shapeは行列の其々の長さを、sizeはデータの総数を表します。
例えば、df = pd.DataFrame([[0,1,2,3,4],[5,6,7,8,9]])
とすると、shapeやsizeは以下のようになります。
df.shape
確かに、2行5列です。
(2, 5)
次にsizeプロパティを出力します。
df.size
確かに、データの数は10個ですね。
10
DataFrameのインデックス名全体を取得する場合はdf.index、カラム名全体を取得した場合はdf.columnsとします。
df.index
Index(['r1', 'r2', 'r3'], dtype='object')
次に、カラム名を取得する場合です。
df.column
Indexとはなっていますが、配列の中身はカラム名ですね。
Index(['c1', 'c2', 'c3'], dtype='object')
最後に、行と列を入れ替える(転置する)メソッドを紹介します。
これはただdf.T
とするだけで行列の入れ替えができます。
df = df.T
df
実際に、行と列が入れ替わりました。
r1 r2 r3
c1 0 3 6
c2 1 4 7
c3 2 5 8
4. データ抽出
ここでは、公的なデータを参照します。
政府の統計資料のうち、日本の各都道府県毎の人口推移がまとめられてあるcsvファイルを取得してきます。
私の場合、dataディレクトリー配下にcsvファイルをコピーしているので、パスを指定してPandasのread_csvメソッドで読み込んでいます。
import pandas as pd
CSV_PATH = "./data/japanese-population/population-per-prefecture.csv"
population_data = pd.read_csv(CSV_PATH, encoding="shift-jis")
population_data = population_data.iloc[0:980]
population_data = population_data.drop("注",axis=1)
population_data.head()
headメソッドは先頭五行を表示するメソッドです。
csvファイルの読取や編集方法はのちの章で解説します。
政府統計資料は以下のようになっています。
都道府県コード 都道府県名 元号 和暦(年) 西暦(年) 人口(総数) 人口(男) 人口(女)
0 00 全国 大正 9.0 1920.0 55963053 28044185 27918868
1 01 北海道 大正 9.0 1920.0 2359183 1244322 1114861
2 02 青森県 大正 9.0 1920.0 756454 381293 375161
3 03 岩手県 大正 9.0 1920.0 845540 421069 424471
4 04 宮城県 大正 9.0 1920.0 961768 485309 476459
そして、データを取得しやすくするために西暦を表すカラム名を変更します。
population_data = population_data.rename(columns={'西暦(年)':"year"})
列や行の指定によるデータの抽出は前章でやりました。
ここでは、条件を指定してデータを取り出す方法を紹介します。
・三通りのデータの抽出方法
Pandasでは、ブランケットに条件を入れる方法と、loc・ilocに条件を入れる方法、そしてqueryメソッドを用いる方法があります。
ブランケットに条件式を入れる方法は、Seriesを解説した章のところと同じように、TrueかFalseとして行や列上のデータが評価されるようにします。
例えば、西暦が2000年のデータだけ抽出するなら以下の通りになります。
population_data[population_data["year"] == 2000].head()
列を指定したいときは、始めに列を指定してから条件を指定するか、条件を満たす行を抽出してから列を指定するかの二通りあります。
まず前者は以下のようになります。
population_data[["西暦(年)","元号"]][population_data["西暦(年)"] == 2000].head()
実行結果は以下の通りです。
二列だけ抽出できています。
西暦(年) 元号
781 2000.0 平成
782 2000.0 平成
783 2000.0 平成
784 2000.0 平成
785 2000.0 平成
ただしこの場合は、条件に必要な列も含めておかなければならないので、関心のない列データまで含めなければなりません。
これを避けたいときは後者のように、後から列を指定することもできます。
population_data[population_data["西暦(年)"] == 2000][["元号"]].head()
元号だけ取得できています。
元号
781 平成
782 平成
783 平成
784 平成
785 平成
loc・ilocを用いる方法は、以下の通りです。
population_data.loc[population_data["元号"] != "平成"]
配列の一つ目にある条件を指定します。
データを抽出するときに列を指定するならば、その方法は主に三通りほどあります。
一つ目ははじめから列を指定する方法。
二つめは行を抽出し終わった後にカラム名を指定する方法です。
三つ目は行を抽出する際に、特定の列だけのデータだけを指定する方法です。
一つ目と二つめはブランケットに条件を入れるときと同じです。
まず、一つ目は以下の通りです。
population_data[["元号","year"]].loc[population_data["元号"] != "平成"]
始めに指定した元号の列まで出力結果に含まれるのは先程と同じですね。
元号 year
1 大正 1920.0
2 大正 1920.0
3 大正 1920.0
4 大正 1920.0
5 大正 1920.0
.. .. ...
676 昭和 1985.0
677 昭和 1985.0
678 昭和 1985.0
679 昭和 1985.0
680 昭和 1985.0
[680 rows x 2 columns]
抽出後に列を指定する方法は以下の通りです。
population_data.loc[population_data["元号"] != "平成"][["year"]]
year
1 1920.0
2 1920.0
3 1920.0
4 1920.0
5 1920.0
.. ...
676 1985.0
677 1985.0
678 1985.0
679 1985.0
680 1985.0
[680 rows x 1 columns]
loc・ilocは、列を指定したいとき、条件を満たす行を抽出する処理と同時に特定の列のデータだけを抜き取るように指示することができます。
配列の第二インデックスに欲しい列を指定するだけで大丈夫です。
population_data.loc[population_data["元号"] != "平成", ["year"]]
一つの列だけを指定したいのであれば、配列ではなくひとつのカラム名だけでも大丈夫です。
year
1 1920.0
2 1920.0
3 1920.0
4 1920.0
5 1920.0
.. ...
676 1985.0
677 1985.0
678 1985.0
679 1985.0
680 1985.0
[680 rows x 1 columns]
最後に、queryメソッドを用いる方法です。
これはsqlで言うwhereと似たような挙動をします。
ポイントは、文字列をそのまま渡してあげる必要があるということです。
population_data.query("元号 != '平成'").head()
この実行結果は、ブランケットに条件式を入れた場合や、loc・ilocを用いた時と同じです。
・便利なメソッド
関数を用いて、データの中央値と一致するデータのみ抽出することもできます。以下の例では、最大値を取得するmaxという関数を使いますが、これ以外にもsumやmin、mean、medianなどの関数もあります。
population_data[population_data["year"] == population_data["year"].max()].head()
ある列がある値のどれかを持つかどうかを調べる際は、isinメソッドを用います。例えば、以下の通りです。
population_data[population_data["year"].isin([2000,2001])].head()
ある列が文字列データだとして、特定の文字を含んでいるかどうかを調べるなら、str.contains("")の形で求めることができます。
population_data[population_data["都道府県名"].str.contains("山")].head()
ある文字で始まるならばstartswith、ある文字で終わるならばendswith、正規表現で検索したいならmatchを、containsの代わりに呼び出せば文字で検索できます。
・複数の条件で求める
条件式を「&」や「|」などのビット演算子で結合することで、複数の条件による絞り込みができます。
例えば、以下の通りです。
population_data[(population_data["元号"] == "平成") & (population_data["人口(総数)"] >= population_data["人口(総数)"].mean()) ].head()
なお、「&」や「|」はビット演算子と呼ばれ、「^」が排他的論理和、「~」が補集合を表します。
5. データ編集
ここでは、データを並び替えたりグループでまとめたり、クロス集計表をつくったり、時系列データに基づいて加工したりといった方法を紹介します。
まず、データの編集用に基礎となるDataFrameを作ります。
import pandas as pd
import random
dates = pd.date_range("2024/04/01",periods=25,freq="D")
employee_id = [ "a0" + str(i) if i < 10 else "a" + str(i) for i in range(1,26)]
male_names = ["大翔", "陽斗", "樹", "大和", "陸", "颯太", "大輝", "翔", "悠斗", "翼",
"太一", "健太", "悠", "颯", "悠人", "悠真", "陽翔", "瑛太", "優斗", "琉生",
"琉星", "遥斗", "悠太", "陽太", "大悟"]
female_names = ["美咲", "凛", "結菜", "優菜", "芽依", "愛", "心", "凜", "莉子", "咲希",
"美羽", "莉愛", "彩", "真央", "瞳", "愛美", "優花", "瑠菜", "莉乃", "萌",
"未来", "優", "結愛", "紗希", "美音"]
names = random.sample(male_names + female_names, 25)
commodities = random.sample(["ガウン","ニット","ジャケット","スーツ","セーター"] * 5 ,25)
prices = {"ガウン":8800,"ニット":5000,"ジャケット":9000,"スーツ":8500,"セーター":4500}
data = []
for j in range(25):
gender = "男" if names[j] in male_names else "女"
price = prices[commodities[j]]
amount = random.randrange(1,11)
profit = price * amount
index = [dates[j],employee_id[j],names[j],gender,commodities[j],price,amount,profit]
# 一つの行のデータを準備しています。
data.append(index)
sales = pd.DataFrame(data,columns=["date","employee_id","name","gender","goods","price","amount","profit"])
sales.head()
上の例では、profitは行に入れるデータを準備する段階で用意していましたが、sales["profit"] = sales["price"] * sales["amount"]
の形で後から代入もできます。
結果は以下の通りになります。
date employee_id name gender goods price amount profit
0 2024-04-01 a01 陽斗 男 スーツ 8500 2 17000
1 2024-04-02 a02 瞳 女 ジャケット 9000 6 54000
2 2024-04-03 a03 大和 男 ガウン 8800 6 52800
3 2024-04-04 a04 凛 女 セーター 4500 1 4500
4 2024-04-05 a05 凜 女 ガウン 8800 5 44000
・データの並び替え
行を並び替えるには、基準の列を指定して値を入れ替えるsort_values
メソッドや、インデックス名に基づいて並び替えるsort_index
メソッドがあります。
sort_values
メソッドは以下の通りです。
sales.sort_values(by=["amount","profit"],ascending=[True,False],inplace=True)
基準にしたい列が二つあったら、配列として基準にしたい列を渡せば大丈夫です。この場合は、amountでまず並び替えられ、同じamountのなかであればprofitで並び替えられるようになっています。
ascendingは昇順・降順を制御するもので、inplaceは、いわばこのメソッドを破壊的メソッドにする引数です。
・グルーピング
単に、重複を排除してある列のデータを取得したいという時は、uniqueメソッドを使います。
sales["goods"].unique()
これはDataFrameではなくSeriesでないとuniqueメソッドを呼び出せないので注意です。
['ジャケット' 'ガウン' 'セーター' 'スーツ' 'ニット']
しかしこれだけはデータを重複なく取得するだけなので、各商品について他のデータを集計するなら別の処理が必要になります。
あるデータの種類に従ってデータをまとめたいという時、groupbyメソッドを使います。
groupbyだけだと、種類ごとにDataFrameをまとめたオブジェクトができるだけなので、何らかの関数を呼び出すことで結果を出力させる必要があります。
具体的には以下の通りです。
sales.groupby(by="goods").mean(numeric_only=True)
商品別にprice、 amount、 profitの平均が出てきます。
price amount profit
goods
ガウン 8800.0 6.2 54560.0
ジャケット 9000.0 8.0 72000.0
スーツ 8500.0 4.6 39100.0
セーター 4500.0 5.6 25200.0
ニット 5000.0 8.8 44000.0
meanの他にも、個数を数えるcount関数、グループの中でn番目のデータを取得するnth関数、複数の関数やユーザー定義関数を渡せるaggメソッドなどがあります。
nth関数は以下の通りです。
sales.groupby(by="goods").nth(4)[["employee_id","name","goods"]]
引数に渡しているのは4ですが、0から数える仕様になっているので五番目を指定しています。
employee_id name goods
15 a16 悠斗 セーター
20 a21 颯太 ニット
22 a23 真央 スーツ
23 a24 美羽 ジャケット
24 a25 莉乃 ガウン
最後にaggメソッドです。
これは標準で組み込まれている関数を呼び出したり、自分でユーザー定義関数を渡したりもできます。
sales[["goods","price","amount","profit"]].groupby(by="goods").agg(["mean","sum"])
始めに指定した、price, amount, profitの其々について、平均と合計を算出させています。
price amount profit
mean sum mean sum mean sum
goods
ガウン 8800.0 44000 6.2 31 54560.0 272800
ジャケット 9000.0 45000 8.0 40 72000.0 360000
スーツ 8500.0 42500 4.6 23 39100.0 195500
セーター 4500.0 22500 5.6 28 25200.0 126000
ニット 5000.0 25000 8.8 44 44000.0 220000
ユーザー定義関数は、辞書型のデータのバリューとして、直接渡すこともできます。
以下の例ではラムダ関数を渡していますが、もっと込み入った処理をしたいのであれば無名関数ではなくてちゃんと定義した名前付きの関数を渡すこともできます。
sales[["goods","price"]].groupby(by="goods").agg({"price":lambda p: min(p) * 1.1})
価格は一つの商品につき一つしかないので、maxでもminでも同じ値が出てきます。
price
goods
ガウン 9680.0
ジャケット 9900.0
スーツ 9350.0
セーター 4950.0
ニット 5500.0
・pivot_table
pivot_tableは、クロス集計表を表示するメソッドです。
sales_pivot = sales.pivot_table(index="gender",columns="goods",values="profit",aggfunc=["sum"])
sales_pivot
縦方向と横方向のどちらにもgroupbyをして種類別に集計をしているようなイメージになると思います。
aggfuncにたいして、配列の中に渡したい関数を複数記述することで、sum以外の計算結果も表示してくれます。
今回の例では複雑になるだけなのでsumだけを渡しています。
sum
goods ガウン ジャケット スーツ セーター ニット
gender
女 132000 54000 85000 90000 90000
男 193600 270000 85000 31500 80000
・時系列データの取り扱い
まず、時系列データに変換する方法から紹介します。
今回の例では、始めから日時をdatetime型で登録していましたが、csvファイルやエクセルファイルを読み込んだ時、これがobjectとして取り込まれることもあります。
そんな時は、to_datetimeメソッドを用いて日付型へ変換する必要があります。
sales_date = pd.to_datetime(sales["date"], format="%Y/%m/%d")
sales_date
format引数は、日時のデータを読み取る際に、元データの日時の形がどうなっているかを示すものです。
0 2024-04-01
1 2024-04-02
2 2024-04-03
3 2024-04-04
4 2024-04-05
5 2024-04-06
6 2024-04-07
7 2024-04-08
8 2024-04-09
9 2024-04-10
日付が取得できました。
もし、年や月にしか興味がないという時は、三通りの編集方法があります。
sales["date"] = pd.to_datetime(sales["date"]).dt.year
sales["date"] = pd.to_datetime(sales["date"]).dt.strftime("%Y")
sales["date"] = pd.DatetimeIndex(pd.to_datetime(sales["date"])).year
いずれの形であっても、一旦datetime型にしておかなければならないので注意です。
DataFrameは、日付がindexになることによって、便利な操作ができるようになります。
まずは、日時をインデックスにしましょう。
sales = sales.set_index(keys=sales_date)
sales = sales.drop(columns=["date"])
sales
自動的なナンバリングではなく、日時データがインデックスになりました。
employee_id name gender goods price amount profit
date
2024-04-01 a01 琉生 男 セーター 4500 6 27000
2024-04-02 a02 悠太 男 ニット 5000 4 20000
2024-04-03 a03 芽依 女 ニット 5000 8 40000
2024-04-04 a04 翼 男 セーター 4500 2 9000
2024-04-05 a05 心 女 セーター 4500 5 22500
便利なのは、何日か事、月ごとなどで集計できる点です。
sales.resample("5D").sum()[["profit"]]
「5D」は五日ごとに集計することを表現しています。
profit
date
2024-04-01 263600
2024-04-06 186700
2024-04-11 179000
2024-04-16 186500
2024-04-21 198700
もし、特定の曜日だけを求めたいのであれば、0~6までの数値との比較で求めることができます。
sales[sales.index.weekday == 6]
sales[sales.index.weekday >= 3]
0が月曜で、6が日曜になります。
6. ファイルの読込と前処理
前の章で、政府の人口統計資料をさらっと読み込んでいましたが、実はこの統計資料には、人口の列に欠損値が含まれています。
空欄だったなら読み込むとき、欠損値としてちゃんとNaNとかが入るのですが、政府の資料はハイフンを欠損値として代替していました。
こういうのはちょっと面倒で、文字列が列のデータに含まれていることになりますから、そのままでは集計関数に渡すときにエラーが出てしまったりします。
population_data = pd.read_csv(CSV_PATH, encoding="shift-jis")
# カラム名を指定するなら、names=[]
# インデックスに指定したい列番号があるときは、index_col=で指定。
population_data = population_data.iloc[0:980]
# 余分な行を削除。欲しい行範囲だけ抽出。
population_data = population_data.drop("注",axis=1)
# 要らない行を削除。
# csvへの出力はto_csvメソッド。encodingも、shift-jisを指定するとよい。
population_data.tail()
dropはある列を削除するものです。
注釈には興味がないので、この列を削除しています。
tailは最後尾5行を出力します。headとは反対ですね。
もしランダムに取得したいならsampleを用います。
実行結果としては以下のようになります。
都道府県コード 都道府県名 元号 和暦(年) 西暦(年) 人口(総数) 人口(男) 人口(女)
975 43 熊本県 平成 27.0 2015.0 1786170 841046 945124
976 44 大分県 平成 27.0 2015.0 1166338 551932 614406
977 45 宮崎県 平成 27.0 2015.0 1104069 519242 584827
978 46 鹿児島県 平成 27.0 2015.0 1648177 773061 875116
979 47 沖縄県 平成 27.0 2015.0 1433566 704619 728947
見た目的には悪くありません。
しかし、もし人口の列にハイフンがなければ、人口の列は数値として扱われているはずです。
それを確かめるために、infoメソッドを呼び出しましょう。
population_data.info()
0 都道府県コード 980 non-null object
1 都道府県名 980 non-null object
2 元号 980 non-null object
3 和暦(年) 980 non-null float64
4 西暦(年) 980 non-null float64
5 人口(総数) 980 non-null object
6 人口(男) 980 non-null object
7 人口(女) 980 non-null object
dtypes: float64(2), object(6)
memory usage: 61.4+ KB
nullはないけど、objectになっています。
これは問題です。
このような場合、欠損値を埋める方法として二通りほど考えられます。
まずは、「-」をNone、ちゃんとした欠損値に置き換えて、欠損値にある値を埋めるメソッドを用いる方法です。
まず、Noneに置き換えます。
population_data = population_data.replace("-",None)
population_data.isnull().any()
これで、ちゃんとした欠損値として扱われます。
都道府県コード False
都道府県名 False
元号 False
和暦(年) False
西暦(年) False
人口(総数) True
人口(男) True
人口(女) True
dtype: bool
そして、欠損値を埋めるfillna関数を呼び出せば自動的に穴埋めしてくれます。
population_data = population_data.fillna(method="ffill",axis=0)
pad/ffill が直後のNaN以外の値で置換し、backfill/bfillが直前のNaN以外の値で置換します。
axios=0が、同列上の他のデータで埋めなさいと指示しています。
時系列データならこれでいいかもしれません。
なぜなら、日にちが少し変わった程度で値が大きく変わることは考えにくいためです。
しかしこの統計資料は、ある年における各都道府県別の人口を並べているので、同列上の直前直後のデータというのは、他県の人口を表します。
これは本来の値とは結構ずれた値になるかもしれません。
このため私は、pandasの関数に依らない欠損値の穴埋め方法を探りました。
結論のコードとしては、以下の通りです。
# 欠損値がnullではなく、-と表記されている!
total_nullables = list(population_data[population_data["人口(総数)"] == "-"].index)
male_nullables = list(population_data[population_data["人口(男)"] == "-"].index)
female_nullables = list(population_data[population_data["人口(女)"] == "-"].index)
nullables = {"人口(総数)":total_nullables,"人口(男)":male_nullables,"人口(女)":female_nullables}
for column,indexs in nullables.items():
for j in indexs:
prefecture_code = population_data.iloc[j]["都道府県コード"]
population_transition = population_data.loc[population_data["都道府県コード"] == prefecture_code, column]
# 人口推移を表す配列を取り出している。
population_transition_index = list(population_transition.index)
# 行番号を配列にしている。
population_data[column][j] = (int(population_transition[population_transition_index[population_transition_index.index(j) + 1]]) + int(population_transition[population_transition_index[population_transition_index.index(j) - 1]]))//2
# 欠損値がある都道府県の、前後の年の人口平均でもって埋めている。
print(population_data[population_data["人口(総数)"] == "-"].index,population_data[population_data["人口(男)"] == "-"].index,population_data[population_data["人口(女)"] == "-"].index)
ここでは、欠損値のある行番号をしらべて同都道府県の前年と次年の行番号を調べていますが、単に欠損値のある年を調べて、+-1 した年で絞り込むこともできたかもしれません。
とりあえず結果としては、前後の年の人口平均で欠損値を埋められました。
Index([], dtype='int64') Index([], dtype='int64') Index([], dtype='int64')
人口を数値として扱いたいので、人口の列データをint型になおす処理をします。
astypeメソッドで、列のデータ型を変えることができます。
population_data["和暦(年)"] = population_data["和暦(年)"].astype(str)
population_data["人口(総数)"] = population_data["人口(総数)"].astype(int)
population_data["人口(男)"] = population_data["人口(男)"].astype(int)
population_data["人口(女)"] = population_data["人口(女)"].astype(int)
population_data.info()
RangeIndex: 980 entries, 0 to 979
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 都道府県コード 980 non-null object
1 都道府県名 980 non-null object
2 元号 980 non-null object
3 和暦(年) 980 non-null object
4 西暦(年) 980 non-null float64
5 人口(総数) 980 non-null int64
6 人口(男) 980 non-null int64
7 人口(女) 980 non-null int64
dtypes: float64(1), int64(3), object(4)
これで、前処理が終わりました!
あとはこれまでの章でまとめたようなメソッドを用いて、特定の行列を抽出したり編集したりできれば、データ処理ライフの始まりです。
7. おわりに
データサイエンスは、前処理が大切とよく言われます。
Pythonを用いてデータ分析をする書籍を開いてみても、その前処理を工夫している様子が書かれています。
表記の揺れを直したり、欠損値を埋めたり、日付をPandasで扱えるdatetime型になおしたりといった操作が前処理の主な部分になると思いますが、それぞれやり方は一つではないようです。
実際に使う時はその都度処理方法を工夫して、ノウハウを蓄積していきたいと思います。