曖昧なままにしないPandas -主要なメソッドを一通り-

# 1. はじめに
おそらく、Pythonでデータの読込や加工をするときPandasを使うことになると思いますが、ちょっとしたデータ処理にはさほど難しいコーディングが必要なわけではないので、コピペしてちょちょいとコードを変えるだけで動いてくれます。

このため、私はついつい詳しいメソッドやプロパティは勉強せずそのままにしてしまい、何度も調べる羽目になりました。また、同じような実行結果になる方法が何通りもあったりして、どれを使えばよいか迷ったりもしました。

今後研究でも使うかもしれないし、そろそろちゃんと勉強しないといけないなと思い、この度Pandasを一通り勉強してきました。忘備録も兼ねて、簡潔にメソッドやプロパティとその実行結果をまとめてみようと思います。

Pandasの学習にあたっては、[キノコードさんのPandas入門動画](https://www.youtube.com/watch?v=XfoYeWCzjac)を参考にしています。

# 目次
- [1. はじめに](#1-はじめに)
- [2. Series](#2-series)
- [3. DataFrame](#3-dataframe)
- [4. データ抽出](#4-データ抽出)
- [5. データ編集](#5-データ編集)
- [6. ファイルの読込と前処理](#6-ファイルの読込と前処理)
- [7. おわりに](#7-おわりに)

# 2. Series
Seriesはよく、「ラベル付き一次元配列」と言われます。これにカラム名を付けて横に繋ぎ合わせれば、DataFrameになります。

**・Seriesの作り方**
まずは二通り紹介します。一つ目は配列を引数に渡す方法、二つ目はnumpyの一次元配列を引数に渡す方法です。
```python:calc.ipynb
import pandas as pd
series1 = pd.Series([0,1,2,3,4,5])
series1
```
```python:calc.ipynb
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は、各データに対して任意のラベルを割り振ることができます。
例えば以下の通りです。
```python:calc.ipynb
series1.index = ["sora","horii","murakabe","arata","amata","ooki"]
series1
```
上から順にインデックス名が割り振られていることが分かります。
```
sora        0
horii       1
murakabe    2
arata       3
amata       4
ooki        5
dtype: int64
```
各データに対して最初からラベルを割り振っておきたいのであれば、辞書型のデータを引数に渡すか、indexという引数を指定して明示的に渡すこともできます。
まず、辞書型のデータを渡す方法です。
```python:calc.ipynb
dict_series = pd.Series({"sora":1,"ame":2,"toto":5})
dict_series
```
次に、index引数に直接渡す方法です。
```python:calc.ipynb
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からデータを取得したり、特定の条件でデータを取得したりします。
```python:calc.ipynb
series1["sora"]
#複数のデータの取得は、文字列ではなくて配列を渡す形になります。
series1[["sora","ooki"]]
```
条件にあったデータを取得したいならば、その条件を直接指定してあげれば大丈夫です。
```python:calc.ipynb
series1[ series1 > 2 ]
```
実行結果は次のようになります。
```
arata    3
amata    4
ooki     5
dtype: int64
```

**・その他のプロパティやメソッド**
値を取得したいならvaluesプロパティ、配列の長さを求めたいならsizeプロパティにアクセスします。

全体で欠損値があるかどうかを調べるときは、hasnansプロパティ、各indexについてnullがあるかどうかはisnullメソッドを呼び出します。
```python:calc.ipynb
series3 = pd.Series([70,40,20,None])
series3.hasnans
```
hasnansはBoolean値を返却します。
上の例でいえば、Noneが配列の中にあり、これは欠損値として扱われるのでTrueです。
```python:calc.ipynb
series3.isnull()
```
この実行結果は以下の通りになります。
最後のデータが欠損値なので、そこだけTrueになります。
```
0    False
1    False
2    False
3     True
dtype: bool
```

# 3. DataFrame
**・DataFrameの作り方**
データフレームの作り方は、配列や辞書型のデータを渡したり、numpyの配列を渡す方法があります。
まず、配列を渡す方法を示します。
```python:calc.ipynb
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の引数を指定して配列を渡すことでラベリングできます。
```python:calc.ipynb
df = pd.DataFrame([[0,1,2],[3,4,5],[6,7,8]])
df.column = ["c1","c2","c3"]
df.index = ["r1","r2","r3"]
df
```
DataFrameの引数として始めから指定することもできます。
```python:calc.ipynb
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を指定しています。
```python:calc.ipynb
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メソッドを呼び出して再代入します。
```python:calc.ipynb
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として取り出すかの違いがあります。
```python:calc.ipynb
df["c1"]
# Seriesとして取り出し

df[["c1"]]
# DataFrameとして取り出し
```
複数のカラムを指定したいのであれば、必然的にDataFrameとして取り出すことになると思います。

<br />

列を指定したい場合は、locないし、ilocを使います。
locがラベルを指定する方法で、ilocが列番号を指定する方法です。
```python:calc.ipynb
df.loc["r1"]
df.iloc[1]
```
この二つは同じ実行結果になります。
```
c1    3
c2    4
c3    5
Name: r1, dtype: int64
```
実は、locとilocは、列も指定することができます。
例えば、行としては2から3行目だけを選び、列としては1、2列目だけを選びたいときは、以下のようにします。
```python:calc.ipynb
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は以下のようになります。
```python:calc.ipynb
df.shape
```
確かに、2行5列です。
```
(2, 5)
```
次にsizeプロパティを出力します。
```python:calc.ipynb
df.size
```
確かに、データの数は10個ですね。
```
10
```

DataFrameのインデックス名全体を取得する場合はdf.index、カラム名全体を取得した場合はdf.columnsとします。
```python:calc.ipynb
df.index
```
```
Index(['r1', 'r2', 'r3'], dtype='object')
```
次に、カラム名を取得する場合です。
```python:calc.ipynb
df.column
```
Indexとはなっていますが、配列の中身はカラム名ですね。
```
Index(['c1', 'c2', 'c3'], dtype='object')
```

最後に、行と列を入れ替える（転置する）メソッドを紹介します。
これはただ`df.T`とするだけで行列の入れ替えができます。
```python:calc.ipynb
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メソッドで読み込んでいます。
```python:calc.ipynb
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
```

そして、データを取得しやすくするために西暦を表すカラム名を変更します。

```python:calc.ipynb
population_data = population_data.rename(columns={'西暦（年）':"year"})
```

列や行の指定によるデータの抽出は前章でやりました。
ここでは、条件を指定してデータを取り出す方法を紹介します。

**・三通りのデータの抽出方法**
Pandasでは、ブランケットに条件を入れる方法と、loc・ilocに条件を入れる方法、そしてqueryメソッドを用いる方法があります。
ブランケットに条件式を入れる方法は、Seriesを解説した章のところと同じように、TrueかFalseとして行や列上のデータが評価されるようにします。
例えば、西暦が2000年のデータだけ抽出するなら以下の通りになります。
```python:calc.ipynb
population_data[population_data["year"] == 2000].head()
```
列を指定したいときは、始めに列を指定してから条件を指定するか、条件を満たす行を抽出してから列を指定するかの二通りあります。
まず前者は以下のようになります。
```python:calc.ipynb
population_data[["西暦（年）","元号"]][population_data["西暦（年）"] == 2000].head()
```
実行結果は以下の通りです。
二列だけ抽出できています。
```
      西暦（年）  元号
781  2000.0      平成
782  2000.0      平成
783  2000.0      平成
784  2000.0      平成
785  2000.0      平成

```

ただしこの場合は、条件に必要な列も含めておかなければならないので、関心のない列データまで含めなければなりません。

これを避けたいときは後者のように、後から列を指定することもできます。
```python:calc.ipynb
population_data[population_data["西暦（年）"] == 2000][["元号"]].head()
```
元号だけ取得できています。
```
     元号
781  平成
782  平成
783  平成
784  平成
785  平成
```

loc・ilocを用いる方法は、以下の通りです。
```python:calc.ipynb
population_data.loc[population_data["元号"] != "平成"]
```
配列の一つ目にある条件を指定します。
データを抽出するときに列を指定するならば、その方法は主に三通りほどあります。
一つ目ははじめから列を指定する方法。
二つめは行を抽出し終わった後にカラム名を指定する方法です。
三つ目は行を抽出する際に、特定の列だけのデータだけを指定する方法です。
一つ目と二つめはブランケットに条件を入れるときと同じです。
まず、一つ目は以下の通りです。

```python:calc.ipynb
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]
```
抽出後に列を指定する方法は以下の通りです。
```python:calc.ipynb
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は、列を指定したいとき、条件を満たす行を抽出する処理と同時に特定の列のデータだけを抜き取るように指示することができます。
配列の第二インデックスに欲しい列を指定するだけで大丈夫です。
```python:calc.ipynb
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と似たような挙動をします。
ポイントは、文字列をそのまま渡してあげる必要があるということです。
```python:calc.ipynb
population_data.query("元号 != '平成'").head()
```
この実行結果は、ブランケットに条件式を入れた場合や、loc・ilocを用いた時と同じです。

**・便利なメソッド**
関数を用いて、データの中央値と一致するデータのみ抽出することもできます。以下の例では、最大値を取得するmaxという関数を使いますが、これ以外にもsumやmin、mean、medianなどの関数もあります。

```python:calc.ipynb
population_data[population_data["year"] == population_data["year"].max()].head()
```
ある列がある値のどれかを持つかどうかを調べる際は、isinメソッドを用います。例えば、以下の通りです。
```python:calc.ipynb
population_data[population_data["year"].isin([2000,2001])].head()
```
ある列が文字列データだとして、特定の文字を含んでいるかどうかを調べるなら、str.contains("")の形で求めることができます。
```python:calc.ipynb
population_data[population_data["都道府県名"].str.contains("山")].head()
```
ある文字で始まるならばstartswith、ある文字で終わるならばendswith、正規表現で検索したいならmatchを、containsの代わりに呼び出せば文字で検索できます。

**・複数の条件で求める**
条件式を「&」や「|」などのビット演算子で結合することで、複数の条件による絞り込みができます。
例えば、以下の通りです。
```python:calc.ipynb
population_data[(population_data["元号"] == "平成") & (population_data["人口（総数）"] >= population_data["人口（総数）"].mean()) ].head()
```
なお、「&」や「|」はビット演算子と呼ばれ、「^」が排他的論理和、「~」が補集合を表します。

# 5. データ編集
ここでは、データを並び替えたりグループでまとめたり、クロス集計表をつくったり、時系列データに基づいて加工したりといった方法を紹介します。
まず、データの編集用に基礎となるDataFrameを作ります。
```python:calc.ipynb
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`メソッドは以下の通りです。
```python:calc.ipynb
sales.sort_values(by=["amount","profit"],ascending=[True,False],inplace=True)
```
基準にしたい列が二つあったら、配列として基準にしたい列を渡せば大丈夫です。この場合は、amountでまず並び替えられ、同じamountのなかであればprofitで並び替えられるようになっています。
ascendingは昇順・降順を制御するもので、inplaceは、いわばこのメソッドを破壊的メソッドにする引数です。

**・グルーピング**
単に、重複を排除してある列のデータを取得したいという時は、uniqueメソッドを使います。
```python:calc.ipynb
sales["goods"].unique()
```
これはDataFrameではなくSeriesでないとuniqueメソッドを呼び出せないので注意です。
```
['ジャケット' 'ガウン' 'セーター' 'スーツ' 'ニット']
```
しかしこれだけはデータを重複なく取得するだけなので、各商品について他のデータを集計するなら別の処理が必要になります。

<br />

あるデータの種類に従ってデータをまとめたいという時、groupbyメソッドを使います。
groupbyだけだと、種類ごとにDataFrameをまとめたオブジェクトができるだけなので、何らかの関数を呼び出すことで結果を出力させる必要があります。
具体的には以下の通りです。
```python:calc.ipynb
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関数は以下の通りです。
```python:calc.ipynb
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メソッドです。
これは標準で組み込まれている関数を呼び出したり、自分でユーザー定義関数を渡したりもできます。
```python:calc.ipynb
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
```
ユーザー定義関数は、辞書型のデータのバリューとして、直接渡すこともできます。
以下の例ではラムダ関数を渡していますが、もっと込み入った処理をしたいのであれば無名関数ではなくてちゃんと定義した名前付きの関数を渡すこともできます。
```python:calc.ipynb
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は、クロス集計表を表示するメソッドです。

```python:calc.ipynb
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メソッドを用いて日付型へ変換する必要があります。
```python:calc.ipynb
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
```
日付が取得できました。
もし、年や月にしか興味がないという時は、三通りの編集方法があります。
```python:calc.ipynb
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になることによって、便利な操作ができるようになります。
まずは、日時をインデックスにしましょう。
```python:calc.ipynb
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
```

便利なのは、何日か事、月ごとなどで集計できる点です。
```python:calc.ipynb
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までの数値との比較で求めることができます。
```python:calc.ipynb
sales[sales.index.weekday == 6]
sales[sales.index.weekday >= 3]
```
0が月曜で、6が日曜になります。

# 6. ファイルの読込と前処理
前の章で、政府の人口統計資料をさらっと読み込んでいましたが、実はこの統計資料には、人口の列に欠損値が含まれています。
空欄だったなら読み込むとき、欠損値としてちゃんとNaNとかが入るのですが、政府の資料はハイフンを欠損値として代替していました。
こういうのはちょっと面倒で、文字列が列のデータに含まれていることになりますから、そのままでは集計関数に渡すときにエラーが出てしまったりします。
```python:calc.ipynb
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メソッドを呼び出しましょう。
```python:calc.ipynb
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に置き換えます。
```python:calc.ipynb
population_data = population_data.replace("-",None)
population_data.isnull().any()
```
これで、ちゃんとした欠損値として扱われます。
```
都道府県コード    False
都道府県名      False
元号         False
和暦（年）      False
西暦（年）      False
人口（総数）      True
人口（男）       True
人口（女）       True
dtype: bool
```
そして、欠損値を埋めるfillna関数を呼び出せば自動的に穴埋めしてくれます。
```python:calc.ipynb
population_data = population_data.fillna(method="ffill",axis=0)
```
pad/ffill が直後のNaN以外の値で置換し、backfill/bfillが直前のNaN以外の値で置換します。
axios=0が、同列上の他のデータで埋めなさいと指示しています。

時系列データならこれでいいかもしれません。
なぜなら、日にちが少し変わった程度で値が大きく変わることは考えにくいためです。
しかしこの統計資料は、ある年における各都道府県別の人口を並べているので、同列上の直前直後のデータというのは、他県の人口を表します。
これは本来の値とは結構ずれた値になるかもしれません。
このため私は、pandasの関数に依らない欠損値の穴埋め方法を探りました。

結論のコードとしては、以下の通りです。
```python:calc.ipynb
# 欠損値が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メソッドで、列のデータ型を変えることができます。
```python:calc.ipynb
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型になおしたりといった操作が前処理の主な部分になると思いますが、それぞれやり方は一つではないようです。
実際に使う時はその都度処理方法を工夫して、ノウハウを蓄積していきたいと思います。