pandasでのファイル読み込みで、デフォルトパラメータを使うことに注意したほうがよいもの

この資料は、[Python東海 第44回勉強会](https://connpass.com/event/300146/)のライトニングトークで発表した「[pandas便利だけどデフォルトパラメータでファイルを読み込むな！](https://www.slideshare.net/maraigue/pandas-a1ec)」をもとに、一部内容を修正するとともにQiita向けに書き直したものです。

# はじめに

[pandas - Python Data Analysis Library](https://pandas.pydata.org/)

pandasという、PythonでCSVなどの表形式のデータを読み込むときに定番のライブラリがあります。  
これはデータの型を判断してそのデータ型として読み込んでくれたり（例えば、数値とみなせる内容が書かれていたら数値型で表現してくれる）、表の内容を行名や列名でアクセスできるようにしてくれたりと便利なのですが、**デフォルトパラメータのまま使ってしまうと、意図しない結果が出るということにたびたび遭遇している**のです。

本記事では、私の経験上「これはデフォルトパラメータで使うな」あるいは「デフォルトパラメータでよいのか必ず確認してから使え」というものを紹介します。

# 個人用テンプレ

```py
pd.read_csv(ファイル名,
    header=0,
    index_col=False,
    keep_default_na=False,
    dtype=None)
```

# はじめに：pandasの特徴

```
Name,Age,Birthplace
Alice,20,Aichi
Bob,30,Gifu
Charles,40,Mie
```

というCSVファイルが `info.csv` という名前で保存されているとき、

```py
import pandas as pd
data = pd.read_csv("info.csv", header=0, index_col=0)
```

とすると、

|Name|Age|Birthplace|
|---|---|---|
|__Alice__|20|Aichi|
|__Bob__|30|Gifu|
|__Charles__|40|Mie|

と扱われます。すなわち先頭の行の内容で列名が指定でき、先頭の列の内容で行名が指定できるようになります。これは「`header=0`」（0行目の内容で列が指定できるようにする）および「`index_col=0`」（0列目の内容で行が指定できるようにする）としているためです。よって、

```py
print(data.at["Alice", "Birthplace"]) # "Aichi" を表示
```

といった具合に値を取り出せます。このような機能は標準ライブラリにおいても、列名でのアクセスについては[csv.DictReader](https://docs.python.org/ja/3/library/csv.html)でできますが、行名は少々面倒です。

また以下のように型の自動判定もしてくれます。

```py
print(type(data.at["Alice", "Age"])) # <class 'numpy.int64'> を表示
print(type(data.at["Alice", "Birthplace"])) # <class 'str'> を表示
```

# デフォルトパラメータを使うことに注意したほうがよいもの

[pandas.read_csv](https://pandas.pydata.org/docs/reference/api/pandas.read_csv.html)関数をもとに説明します。これは名前の通りCSVファイルを読み込むものですが、他にも様々な表形式のファイルに対応した関数が用意されています。

ちなみにですが、pandas.read_csvのパラメータはこんなにあります…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49715/c3aaf814-926f-1663-0d5b-9c2457df2741.png)

## header

前記の例の通り、これを指定することで、**表の中の特定の行が列名を表していると扱う**ことができます。  
私が使用する際は、`header=0`（ファイル内の最初の行が列名）か`header=None`（ファイル内に列名は書かれておらず、最初の行も含めて実際の値が書き込まれている）を指定しています。

デフォルトのまま使うと、最初の行がそう扱われます。ただし「names」パラメータで列名を別途与えた場合に限っては、ファイル中からは列名は取得されず、最初の行も含めて実際の値と扱います。

これはそもそも、ファイルの内容をちゃんと確認して使うべきところなので、デフォルトで使うのは良くないと考えています。また他の人がコードを見たときに備えて、「これは最初の行が列名が書かれている（いない）ファイルなんだ」と明示しておくべきと思います。

## index_col

これも前記の例の通り、これを指定することで、**表の中の特定の列が行名を表している**と扱うことができます。  
私が使用する際は、`index_col=0`（最初の行が列名）か`index_col=False`（ファイルに行名は書かれておらず、最初の列も含めて実際の値が書き込まれている）を指定しています。**headerと異なり、ファイル内に行名が書かれていないと扱いたいときは、NoneではなくFalseを指定する必要があります**。

デフォルトのまま使うと、最初の列がそう扱われます。

headerと同様、ファイルの内容をちゃんと確認して使うべきところなので、デフォルトで使うのは良くないと考えています。ただそれだけでなく、`index_col=False` については次のような好影響もあります。

pandasではファイルを読み込む際、

-   「0行目の列数<1行目の列数」なら、0行目の最初のセルが省略されたものとみなされる。
-   「0行目の列数>=1行目の列数」なら、0行目の最初のセルはそのまま左上に置かれる。

という仕様があります。どういうことかというと、例えばCSVファイルが

```
Name,Age
Alice,20,Aichi
Bob,30,Gifu
Charles,40,Mie
```

のように0行目の要素数が以降の行よりも少ない場合、pandasで読み込んだ結果が

||Name|Age|
|---|---|---|
|Alice|20|Aichi|
|Bob|30|Gifu|
|Charles|40|Mie|

と、「左上が省略されたCSVファイルである」とみなされるのです。`index_col=False` のときにこれが起きていると、（上記の通り読み込みはしますが）警告を出してくれるのです。  
（なお、`index_col=0` の場合は警告にはならず、単に上記の通り読み込んでしまいます）

## na_values / keep_default_na

pandasは自動的に型を推定するということを書きましたが、このうちファイル中に**どのような文字列が書き込まれていると、無効値**（Python上では `nan`）**へと変換されるか**を規定するためのパラメータです。  
私が使用する際は、中身の素性がわかっている（例えば、自分のプログラムで生成した）ファイルを読み込む際には省略することもありますが、そうでないときは `keep_default_na=False` とし、無効値が出ない（単なる文字列とみなす）ようにしています。

デフォルトで何が無効値になるかは、pandas.read_csvのドキュメント https://pandas.pydata.org/docs/reference/api/pandas.read_csv.html の、「na_values」の項に一覧があるのでご覧ください。

どのような場合に問題になるかというと、**ある列の他の内容が普通の文字列であったとしても、その中に空欄や「NA」という文字列などがあると無効値扱いになる**（もちろん文字列型ではなくなるので、他の文字列との演算もできなくなる）という不便さがあるのです。なので、無効値扱いしたい値がないとわかっているファイルについては、`keep_default_na=False` を指定しておき、無効値が出ないようにするのが安全と考えています。

## dtype

**データ型を列単位で指定する**ためのパラメータです。例えば

```py
data = pd.read_csv("info.csv",header=0, index_col=0, dtype={"Age": str})
```

とすると、Age列は（数値と解釈できる値が書かれているものの）文字列型と扱われます。

これは、ファイル中に「意味上、数値や文字列しか入らないことが明らかな列」がある場合に指定しています。pandasでは**列の内容を、なるべく当該列の全内容が表現できるような型に変換する**ためです。例えばCSVファイルが

```
Name,Age,Birthplace
Alice,20,Aichi
Bob,30,Gifu
Charles,Forty,Mie
```

と、Ageが基本は数値な中で「Forty」というのが混ざっている場合、数値と扱える「20」や「30」も文字列と読み込まれてしまいます。`dtype={"Age":int}` と指定しておけば、この場合はエラーで弾いてくれます。

なお、**このオプションよりも、前述のna_values / keep_default_naの指定の方が優先されます**。強制的に文字列とみなしたい列がある場合は、dtypeとkeep_default_naを併用しましょう。

# 個人用テンプレ（再掲）

```py
pd.read_csv(ファイル名,
    header=0,
    index_col=False,
    keep_default_na=False,
    dtype=None)
```

ということで、**これらのパラメータは実質必須**（意図的に省くなら省いてもよいが、何も考えずに省略すべきではない）**と考えています**。