63
62

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【保存版】Pandas2.0のread_csv関数の全引数、パフォーマンス、活用テクニックを完全解説する!

Last updated at Posted at 2023-04-12

はじめに

みずほリサーチ&テクノロジーズ株式会社の@fujineです。

本記事ではpandas 2.0を対象に、CSVファイルの入力関数である read_csvの全49個(!)の引数をじっくり解説 いたします。具体的には、

  • 各引数には、どんな効果や(公式ドキュメントにも記載されていない)制約があるのか?
  • 引数を工夫することで、処理時間やメモリ消費量などのパフォーマンスが具体的にどれだけ改善されるのか?
  • pandasのver2.0では、それ以前のバージョンからどう変化したのか?
  • 多くの引数を保守しやすく管理するにはどうしたらいいか?

を体系的に整理・検証することを目指します。

新入社員/若手社員向けのレクチャーや、これまで「何となく」使っていた引数を「効果的に」使えるようになるためのノウハウ集としてご活用下さい!

read_csvの引数が49個もある理由

「ただCSVを読み込むだけなのに、なぜそんなに多くの引数が必要なのか?」と気になった方も多いでしょう。

CSV(Comma-Separated Values)フォーマットは60年以上前の1960年代頃から使われ始めたらしく、現在でも身近なデータフォーマットの1つです。当時は国際標準が存在しなかったため、CSVの普及とともに、OSやアプリケーション、国や地域、ベンダーの違いによって微妙に異なる独自の記法が多数誕生しました。2005年10月にようやくRFC4180にて国際標準化されましたが、従来の独自記法は今も根強く存在していますread_csvの引数が他の関数と比べて異様に多いのは、これらの独自記法を可能な限り引数で制御しようとした結果となります。

そして、これだけ引数の数が多いとどの引数が重要なのか分かりにくく、初学者がいきなりread_csvを使いこなすのはハードルが高い です。更に、pandasのバージョンアップによって各引数が新規追加・仕様変更・非推奨/廃止になることがあり、使い慣れた経験者であっても 引数の変化を適時検証するのはやはり大変 です。

そこで、read_csvを使いこなすための手引きを目指し、本記事を執筆いたしました。

検証環境

GoogleCloud VMのn2d-highmem-4 インスタンス(4vCPU, 32GBメモリ)で検証します。Linux OSのバージョンは以下の通りです。

$ cat /proc/version
Linux version 4.19.0-23-cloud-amd64 (debian-kernel@lists.debian.org) (gcc version 8.3.0 (Debian 8.3.0-6)) #1 SMP Debian 4.19.269-1 (2022-12-20)

Pythonはver3.11.0pandasver2.0.0を使用します。ver2.0.0は2023/4/3にメジャーアップデートしたばかりで、read_csvに対しても重要な変更がいくつか行われました。

pipコマンドでpandasをインストールします。他のパッケージは任意です。本記事では以下用途で使用します。

  • pyarrow: pyarrowのデータ型として利用
  • s3fs: AWS S3に保管されたオブジェクトへのアクセス用
  • chardet: ファイルの文字コード推定用
  • pyyaml: yamlファイルの入出力用
  • memory_profiler: メモリ消費量の計測用
$ pip install -q pandas==2.0.0 pyarrow s3fs chardet pyyaml memory_profiler

pandasをインポートし、DataFrameの表示オプションを設定します。

import pandas as pd

pd.set_option('display.unicode.east_asian_width', True)
pd.set_option('display.width', 150)

検証のためのサンプルファイルを作成します。中身は、日本の気象データを模したものです。

$ cat sample.csv
Date,Area,MinTemp,MaxTemp,WindSpeed,Rainfall
2023/4/1,東京,10.3,23.3,2.6,null
2023/4/1,大阪,10.4,25.1,2.3,null
2023/4/2,東京,11.1,19.0,3.2,0
2023/4/2,大阪,10.9,23.3,4.4,null

引数の解説

以下より、read_csvの各引数を用途別に解説していきます。

基本

filepath_or_buffer

CSVファイルのパスを指定します。全引数の中で唯一の必須引数です。位置引数でもあるため、filepath_or_buffer='xxx.csv'と指定しなくてもファイルパスとして認識されます。

最も一般的な指定方法は、ファイルパスを文字列で指定する方法でしょう。

df = pd.read_csv('sample.csv')
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

file-likeオブジェクトにも対応しており、標準ライブラリのpathlib.Pathos.pathオブジェクトも読み込めます。

from pathlib import Path

p = Path('sample.csv')
df = pd.read_csv(p)
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

io.StringIOにも対応しています。CSVデータがメモリ上にある場合、StringIOを使えば以下のようにメモリ上からダイレクトにDataFrameへ変換できるので便利です。

from io import StringIO

with StringIO("""
Date,Area,MinTemp,MaxTemp,WindSpeed,Rainfall
2023/4/1,東京,10.3,23.3,2.6,null
2023/4/1,大阪,10.4,25.1,2.3,null
2023/4/2,東京,11.1,19.0,3.2,0
2023/4/2,大阪,10.9,23.3,4.4,null
""") as f:
    df = pd.read_csv(f)

print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

ローカル上のファイルだけでなく、URLやクラウドストレージ(S3GCSなど)上にあるリモートファイルも読み込むことができます。

以下は、国土交通省が公開している日本標準時基準の台風発生数データです。カスタムヘッダーが必要な場合は、後述するstorage_optionsを別途指定します。

url = 'https://www.data.jma.go.jp/yoho/typhoon/statistics/generation/generation_j.csv'
df = pd.read_csv(url, encoding='shift-jis')
print(df)
      年  1月  2月  3月  4月  5月  6月  7月  8月  9月  10月  11月  12月  年間
0   1951  NaN  1.0  1.0  2.0  1.0  1.0  3.0    3    2     4   1.0   2.0    21
1   1952  NaN  NaN  NaN  NaN  NaN  3.0  3.0    5    3     6   3.0   4.0    27
2   1953  NaN  1.0  NaN  NaN  1.0  2.0  1.0    6    3     5   3.0   1.0    23
3   1954  NaN  NaN  1.0  NaN  1.0  NaN  1.0    5    5     4   3.0   1.0    21
4   1955  1.0  1.0  1.0  1.0  NaN  2.0  7.0    6    4     3   1.0   1.0    28
..   ...  ...  ...  ...  ...  ...  ...  ...  ...  ...   ...   ...   ...   ...
67  2018  1.0  1.0  1.0  NaN  NaN  4.0  5.0    9    4     1   3.0   NaN    29
68  2019  1.0  1.0  NaN  NaN  NaN  1.0  4.0    5    6     4   6.0   1.0    29
69  2020  NaN  NaN  NaN  NaN  1.0  1.0  NaN    8    3     6   3.0   1.0    23
70  2021  NaN  1.0  NaN  1.0  1.0  2.0  3.0    4    4     4   1.0   1.0    22
71  2022  NaN  NaN  NaN  2.0  NaN  1.0  3.0    5    7     5   1.0   1.0    25

[72 rows x 14 columns]

storage_options

リモートファイルへのアクセスに必要な情報(ホストの接続情報や、HTTP/HTTPSリクエスト時に設定するカスタムヘッダーなど)を辞書で設定します。

以下は、AWS S3で公開されている「Amazonレビューのサンプルデータセット」オブジェクトにアクセスする例です。storage_optionsには、オブジェクトに匿名でアクセスするための{'anon': True}というオプションを付与しています。

url = 's3://amazon-reviews-pds/tsv/sample_us.tsv'
df = pd.read_csv(url, sep='\t', storage_options={'anon': True})
print(df.head(1).T)
                                            0
marketplace                                US
customer_id                          18778586
review_id                       RDIJS7QYB6XNR
product_id                         B00EDBY7X8
product_parent                      122952789
product_title      Monopoly Junior Board Game
product_category                         Toys
star_rating                                 5
helpful_votes                               0
total_votes                                 0
vine                                        N
verified_purchase                           Y
review_headline                    Five Stars
review_body                      Excellent!!!
review_date                        2015-08-31

pandasの公式ドキュメントにて、各クラウド接続に必要な依存パッケージが公開されています。上記以外に指定可能なオプションは、記載パッケージの公式ドキュメントをご参照下さい。

sep / delimiter

区切り文字を指定します(delimitersepのエイリアスです)。デフォルトは,です。

sepには任意の一文字や正規表現を指定できるため、以下のようにカンマ以外の区切り文字にも対応できます。

$ cat sample_sep.csv
Date;Area;MinTemp;MaxTemp;WindSpeed;Rainfall
2023/4/1;東京;10.3;23.3;2.6;null
2023/4/1;大阪;10.4;25.1;2.3;null
2023/4/2;東京;11.1;19.0;3.2;0
2023/4/2;大阪;10.9;23.3;4.4;null
df = pd.read_csv('sample_sep.csv', sep=';')
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

sep='\t'と指定すれば、カンマの代わりにタブ(\t)で区切られたTSVファイルを読み込めます(TSVファイル専用のpandas.read_table関数でも同じ結果が得られます)。

$ cat sample_sep.tsv
Date	Area	MinTemp	MaxTemp	WindSpeed	Rainfall
2023/4/1	東京	10.3	23.3	2.6	null
2023/4/1	大阪	10.4	25.1	2.3	null
2023/4/2	東京	11.1	19.0	3.2	0
2023/4/2	大阪	10.9	23.3	4.4	null
df = pd.read_csv('sample_sep.tsv', sep='\t')
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

sep=Noneとすると、データから区切り文字を自動推定してくれます。自動推定するには、engine='python'という引数(後述)も同時に指定します。

df = pd.read_csv('sample_sep.csv', sep=None, engine='python')
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

delim_whitespace

スペースを区切り文字とするかどうかを指定します。デフォルトはFalse(スペースで区切らない)です。

Trueにするとsep='\s+'と等価となり、各フィールドの値がスペースで区切られて読み込まれます。

$ cat sample_space.csv
Date Area MinTemp MaxTemp WindSpeed Rainfall
2023/4/1 東京 10.3 23.3 2.6 null
2023/4/1 大阪 10.4 25.1 2.3 null
2023/4/2 東京 11.1 19.0 3.2 0
2023/4/2 大阪 10.9 23.3 4.4 null
df = pd.read_csv('sample_space.csv', delim_whitespace=True)
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

delim_whitespacesepは、どちらか一方のみを指定します。両方を同時に指定するとValueErrorが送出されます。

try:
    df = pd.read_csv('sample_space.csv', delim_whitespace=True, sep=None)
except Exception as e:
    print(repr(e))
ValueError('Specified a delimiter with both sep and delim_whitespace=True; you can only specify one.')

カラムとインデックスの指定

header

ヘッダーの行番号(0始まり)を指定します。デフォルトはheader='infer'となっており、自動的に推定されます。

以下ファイルのように、ヘッダー領域にコメント行などが含まれている場合を想定します。

$ cat sample_header.csv
# comment
Date,Area,MinTemp,MaxTemp,WindSpeed,Rainfall
2023/4/1,東京,10.3,23.3,2.6,null
2023/4/1,大阪,10.4,25.1,2.3,null
2023/4/2,東京,11.1,19.0,3.2,0
2023/4/2,大阪,10.9,23.3,4.4,null

header引数にヘッダーの行番号を指定します。1以上の値を指定した場合、それより上の行はスキップされます。

# 0行目のコメントをスキップし、1行目をヘッダーとして読み込む
df = pd.read_csv('sample_header.csv', header=1)
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

マルチヘッダーの場合は、ヘッダー行番号のリストを指定することで、カラムがマルチインデックスとなります。

$ cat sample_multiheader.csv
# comment
Date,Area,MinTemp,MaxTemp,WindSpeed,Rainfall
# comment
年月日,観測地点,最低気温,最高気温,風速,降水量
2023/4/1,東京,10.3,23.3,2.6,null
2023/4/1,大阪,10.4,25.1,2.3,null
2023/4/2,東京,11.1,19.0,3.2,0
2023/4/2,大阪,10.9,23.3,4.4,null
# 0行目と2行目をスキップし、1行目と3行目をマルチヘッダーとして読み込む
df = pd.read_csv('sample_multiheader.csv', header=[1, 3])
print(df)
       Date     Area  MinTemp  MaxTemp WindSpeed Rainfall
     年月日 観測地点 最低気温 最高気温      風速   降水量
0  2023/4/1     東京     10.3     23.3       2.6      NaN
1  2023/4/1     大阪     10.4     25.1       2.3      NaN
2  2023/4/2     東京     11.1     19.0       3.2      0.0
3  2023/4/2     大阪     10.9     23.3       4.4      NaN

ヘッダーが無い場合はheader=Noneとします。カラム名には連番が自動付与されますが、後述するnames引数で任意のカラム名で上書きできます。

$ cat sample_noheader.csv
2023/4/1,東京,10.3,23.3,2.6,null
2023/4/1,大阪,10.4,25.1,2.3,null
2023/4/2,東京,11.1,19.0,3.2,0
2023/4/2,大阪,10.9,23.3,4.4,null
df = pd.read_csv('sample_noheader.csv', header=None)
print(df)
          0     1     2     3    4    5
0  2023/4/1  東京  10.3  23.3  2.6  NaN
1  2023/4/1  大阪  10.4  25.1  2.3  NaN
2  2023/4/2  東京  11.1  19.0  3.2  0.0
3  2023/4/2  大阪  10.9  23.3  4.4  NaN

names

カラム名を明示的に指定します。

ヘッダーがあるCSVファイルの場合は、headerでヘッダー行番号を明示的に指定し、namesに別名のリストを与えます。

names = ['年月日', '観測値', '最低気温', '最高気温', '風速', '降水量']
df = pd.read_csv('sample.csv', header=0, names=names)
print(df)
     年月日 観測値  最低気温  最高気温  風速  降水量
0  2023/4/1   東京      10.3      23.3   2.6     NaN
1  2023/4/1   大阪      10.4      25.1   2.3     NaN
2  2023/4/2   東京      11.1      19.0   3.2     0.0
3  2023/4/2   大阪      10.9      23.3   4.4     NaN

ヘッダー無しファイルの場合は、以下のようにheader=Noneとした上でカラム名を指定します。

df = pd.read_csv('sample_noheader.csv', header=None, names=names)
print(df)
     年月日 観測値  最低気温  最高気温  風速  降水量
0  2023/4/1   東京      10.3      23.3   2.6     NaN
1  2023/4/1   大阪      10.4      25.1   2.3     NaN
2  2023/4/2   東京      11.1      19.0   3.2     0.0
3  2023/4/2   大阪      10.9      23.3   4.4     NaN

なお、データのフィールド数とlen(names)が一致しない場合、エラーとならず、以下挙動となります。

len(names)がレコードのフィールド数よりも多い場合、各フィールドの値はnames左側 から順に紐づけられ、余ったカラムの値は全て欠損値となります。

df = pd.read_csv('sample_noheader.csv', header=None, 
                 names=names + ['Extra'])
print(df)
     年月日 観測値  最低気温  最高気温  風速  降水量  Extra
0  2023/4/1   東京      10.3      23.3   2.6     NaN    NaN
1  2023/4/1   大阪      10.4      25.1   2.3     NaN    NaN
2  2023/4/2   東京      11.1      19.0   3.2     0.0    NaN
3  2023/4/2   大阪      10.9      23.3   4.4     NaN    NaN

len(names)がカラム数よりも少ない場合、各フィールドの値はnames右側 から順に紐づけられ、余ったデータはインデックスとなります。

df = pd.read_csv('sample_noheader.csv', header=None, 
                 names=names[:-1])
print(df)
         年月日  観測値  最低気温  最高気温  風速
2023/4/1   東京    10.3      23.3       2.6   NaN
2023/4/1   大阪    10.4      25.1       2.3   NaN
2023/4/2   東京    11.1      19.0       3.2   0.0
2023/4/2   大阪    10.9      23.3       4.4   NaN

namesを指定するとカラムとデータの値がずれてしまう」といった事象が起きたら、データのフィールド数とlen(names)が一致しているか確認しましょう!

index_col

インデックスとするカラムを指定します。デフォルトはNoneです。

カラム名もしくは列番号を指定すると、そのカラムがインデックスとなります。

df = pd.read_csv('sample.csv', index_col='Date')
print(df)
          Area  MinTemp  MaxTemp  WindSpeed  Rainfall
Date                                                 
2023/4/1  東京     10.3     23.3        2.6       NaN
2023/4/1  大阪     10.4     25.1        2.3       NaN
2023/4/2  東京     11.1     19.0        3.2       0.0
2023/4/2  大阪     10.9     23.3        4.4       NaN

index_colにリストを指定すると、マルチインデックスとなります。

df = pd.read_csv('sample.csv', index_col=['Date', 'Area'])
print(df)
               MinTemp  MaxTemp  WindSpeed  Rainfall
Date     Area                                       
2023/4/1 東京     10.3     23.3        2.6       NaN
         大阪     10.4     25.1        2.3       NaN
2023/4/2 東京     11.1     19.0        3.2       0.0
         大阪     10.9     23.3        4.4       NaN

index_col=Falseとすると、カラムのインデックス化を無効化できます。

例えば、以下のようにヘッダー以外のレコード末尾にカンマが付与されたトリッキーなCSVがあるとします。

$ cat sample_malformed.csv
Date,Area,MinTemp,MaxTemp,WindSpeed,Rainfall
2023/4/1,東京,10.3,23.3,2.6,null,
2023/4/1,大阪,10.4,25.1,2.3,null,
2023/4/2,東京,11.1,19.0,3.2,0,
2023/4/2,大阪,10.9,23.3,4.4,null,

そのまま読み込むと、レコードのフィールド数(7)とヘッダーのカラム数(6)が一致しないため、左のDate列が自動的にインデックスとなってしまいます。

df = pd.read_csv('sample_malformed.csv')
print(df)
          Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
2023/4/1  東京  10.3     23.3      2.6        NaN       NaN
2023/4/1  大阪  10.4     25.1      2.3        NaN       NaN
2023/4/2  東京  11.1     19.0      3.2        0.0       NaN
2023/4/2  大阪  10.9     23.3      4.4        NaN       NaN

index_col=FalseとすることでDateカラムはインデックス化されず、正しく読み込むことができます。

df = pd.read_csv('sample_malformed.csv', index_col=False)
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

「CSVファイルを読み込むと、なぜか自動的にカラムがインデックスになってしまう」という事象が起きた際は、フィールド数とカラム数が一致しているか確認しましょう!

usecols

読み込むカラムを、列番号かカラム名のリストで指定します。デフォルトはNoneで、全てのカラムが読み込まれます。

読み込みたいカラムが限定的な場合、usecolsで絞り込むことでメモリ消費量を抑える効果があります。

usecols=['Date', 'Area', 'MinTemp', 'MaxTemp']
df = pd.read_csv('sample.csv', usecols=usecols)
print(df)
       Date  Area  MinTemp  MaxTemp
0  2023/4/1  東京     10.3     23.3
1  2023/4/1  大阪     10.4     25.1
2  2023/4/2  東京     11.1     19.0
3  2023/4/2  大阪     10.9     23.3

usecolsに関数を指定すると、関数がTrue(もしくはTrueと評価されるオブジェクト)を返すカラムのみが読み込まれます。

カラム数が数百~と大量にあると、1つずつ明示的に指定するのは大変です。カラム名にパターンがある場合は、以下のように関数と正規表現を組合わせた方がコードがスッキリします。

$ cat sample_manycolumns.csv
a_1,a_2,a_3,b_1,b_2,b_3,c_1,c_2,c_3,d_1,d_2,d_3
1,2,3,4,5,6,7,8,9,10,11,12
1,2,3,4,5,6,7,8,9,10,11,12
1,2,3,4,5,6,7,8,9,10,11,12
import re

# カラム名の1文字目がa/c/dのいずれか、3文字目が2/3のいずれかのカラムのみ読み込む
usecols = lambda column: re.search('[a|c|d]_[2|3]', column)
df = pd.read_csv('sample_manycolumns.csv', usecols=usecols)
print(df)
   a_2  a_3  c_2  c_3  d_2  d_3
0    2    3    8    9   11   12
1    2    3    8    9   11   12
2    2    3    8    9   11   12

squeeze / prefix / mangle_dupe_cols

これらの引数はver1.5時点で非推奨となっています。同様の処理を行いたい場合は、以下の代替手段を参考にして下さい。

引数名 効果 代替手段
squeeze カラムが1列の場合、Seriesとして読み込む pandas.DataFrame.squeezeを使用
prefix カラム名に接頭辞を追加 pandas.DataFrame.add_prefixを使用
mangle_dupe_cols 重複するカラム名の末尾に連番を付与 新しい引数を検討中(GH#47718)

データ変換

dtype

カラムのデータ型を指定します。デフォルトはNoneで、データから自動的に推定されます。

適切なデータ型を明示的に指定することで、メモリ消費量を抑える効果があります。

カラム名をキー、データ型を値とする辞書をdtypeに指定すると、カラム毎に指定したデータ型に変換されます。データ型には

といった、様々なデータ型を指定することができます。

import numpy as np

dtype={'MinTemp': np.float32, 'MaxTemp': np.float32}
df = pd.read_csv('sample.csv', dtype=dtype)
print(df.dtypes)
Date          object
Area          object
MinTemp      float32
MaxTemp      float32
WindSpeed    float64
Rainfall     float64
dtype: object

dtypeに単一のデータ型を指定すると、全カラムが同じデータ型に変換されます。

df = pd.read_csv('sample.csv', dtype=str)
print(df.dtypes)
Date         object
Area         object
MinTemp      object
MaxTemp      object
WindSpeed    object
Rainfall     object
dtype: object

変換できない値が含まれているとValueErrorが送出され、変換に失敗した列番号がエラーメッセージに出力されます。

try:
    df = pd.read_csv('sample.csv', dtype=bool)
except Exception as e:
    print(repr(e))
ValueError('cannot safely convert passed user dtype of bool for object dtyped data in column 0')

dtype_backend

データ型のバックエンドを指定します。ver2.0で新たに追加された実験的な引数です。

通常はnumpyのデータ型が採用されますが、新たにnumpy_nullablepyarrowの2つを指定できるようになりました。

まずは、バックエンドをnumpy_nullableで読み込んでみます。

df = pd.read_csv('sample.csv', dtype_backend='numpy_nullable')
    
print(df)
print('-' * 60)
print(df.dtypes)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6      <NA>
1  2023/4/1  大阪     10.4     25.1        2.3      <NA>
2  2023/4/2  東京     11.1     19.0        3.2         0
3  2023/4/2  大阪     10.9     23.3        4.4      <NA>
------------------------------------------------------------
Date         string[python]
Area         string[python]
MinTemp             Float64
MaxTemp             Float64
WindSpeed           Float64
Rainfall              Int64
dtype: object

欠損値がNaNではなく<NA>となりました。<NA>とは、pandasver1.0から採用された新しい欠損値です。

df.dtypesでデータ型を確認すると、整数カラムと浮動小数点数カラムのデータ型が従来のint64/float64ではなく、Int64/Float64(いずれも先頭が大文字)となっています。Int64(Int64Dtype)Float64(Float64Dtype)は、どちらも<NA>をサポートする新しいデータ型です。

続いて、バックエンドをpyarrowで読み込んでみます。

df = pd.read_csv('sample.csv', dtype_backend='pyarrow')

print(df)
print('-' * 60)
print(df.dtypes)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6      <NA>
1  2023/4/1  大阪     10.4     25.1        2.3      <NA>
2  2023/4/2  東京     11.1     19.0        3.2         0
3  2023/4/2  大阪     10.9     23.3        4.4      <NA>
------------------------------------------------------------
Date         string[pyarrow]
Area         string[pyarrow]
MinTemp      double[pyarrow]
MaxTemp      double[pyarrow]
WindSpeed    double[pyarrow]
Rainfall      int64[pyarrow]
dtype: object

欠損値が<NA>なのはnumpy_nullableと同じですが、データ型は全てpyarrowデータ型のラッパーであるpandas.ArrowDtypeになりました。

pyarrowでは他にも、

  • 日付を32bitで表現するdate32
  • 任意の有効桁数を指定可能なdecimal128
  • バイナリデータに特化したbinary

など、pandasnumpyには無い多様なデータ型を多数提供しています。pandasを使いつつpyarrowのデータ型も活用したい、というシーンに有効です。

なお、dtype_backenddtypeを同時に指定した場合、dtypeで指定したデータ型が優先されます。

df = pd.read_csv('sample.csv', dtype_backend='pyarrow',
                 dtype={'Area': 'category', 'MinTemp': np.float32})

print(df)
print('-' * 60)
print(df.dtypes)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6      <NA>
1  2023/4/1  大阪     10.4     25.1        2.3      <NA>
2  2023/4/2  東京     11.1     19.0        3.2         0
3  2023/4/2  大阪     10.9     23.3        4.4      <NA>
------------------------------------------------------------
Date         string[pyarrow]
Area                category
MinTemp              float32
MaxTemp      double[pyarrow]
WindSpeed    double[pyarrow]
Rainfall      int64[pyarrow]
dtype: object

engine

CSVパーサーエンジンの種類を選択できます。現在サポートされているエンジンはcpythonpyarrowの3種類です。

各エンジンの特徴は以下の通りです。明示的に指定しない場合はcが適用されます。

エンジン メリット デメリット
c 高速 区切り文字(sep)を自動推定できない
skipfooteron_bad_lines等の他引数と併用できない
python 全引数をサポート 他パーサーより低速
pyarrow 高速
マルチスレッド対応
実験段階であり、多数引数が実装中(GH38872)

ただし、engine='c'と明示的に指定しても、cエンジンが処理できないデータや引数が含まれていた場合は以下のようにParserWarningが送出され、pythonエンジンにフォールバックすることがあります。

df = pd.read_csv('sample_sep.csv', sep=None)
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN


/tmp/ipykernel_6667/3161112102.py:1: ParserWarning: Falling back to the 'python' engine because the 'c' engine does not support sep=None with delim_whitespace=False; you can avoid this warning by specifying engine='python'.
  df = pd.read_csv('sample_sep.csv', sep=None)

各エンジンのパフォーマンス検証結果は、次章をご参照下さい。

converters

カラム毎に個別の変換処理を指定することができます。デフォルトはNoneです。

変換には、キーをカラム名(もしくは列番号)、値を変換関数とする辞書を指定します。

# 最低気温と最高気温を、摂氏(℃)から華氏(℉)に変換
c2f = lambda c: float(c) * 1.8 + 32
df = pd.read_csv('sample.csv', converters={'MinTemp': c2f, 'MinTemp': c2f})
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京    50.54     23.3        2.6       NaN
1  2023/4/1  大阪    50.72     25.1        2.3       NaN
2  2023/4/2  東京    51.98     19.0        3.2       0.0
3  2023/4/2  大阪    51.62     23.3        4.4       NaN

convertersdtypeで同じカラムを同時指定した場合、convertersが優先され、dtypeは無視されます(ParserWarningが送出されます)。

df = pd.read_csv('sample.csv', dtype={'MinTemp': bool},
                 converters={'MinTemp': c2f, 'MinTemp': c2f})
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京    50.54     23.3        2.6       NaN
1  2023/4/1  大阪    50.72     25.1        2.3       NaN
2  2023/4/2  東京    51.98     19.0        3.2       0.0
3  2023/4/2  大阪    51.62     23.3        4.4       NaN


/tmp/ipykernel_6667/259361885.py:1: ParserWarning: Both a converter and dtype were specified for column MinTemp - only the converter will be used.
  df = pd.read_csv('sample.csv', dtype={'MinTemp': bool},

true_values / false_values

True/Falseに置換する値を文字列のリストで指定します。どちらもデフォルトはNoneです。

この置換処理は、置換後のカラムがboolにキャスト可能な場合のみ行われます。

df = pd.read_csv('sample.csv', true_values=['大阪'], 
                 false_values=['東京'])
print(df)
       Date   Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  False     10.3     23.3        2.6       NaN
1  2023/4/1   True     10.4     25.1        2.3       NaN
2  2023/4/2  False     11.1     19.0        3.2       0.0
3  2023/4/2   True     10.9     23.3        4.4       NaN

以下のように、置換後のカラムTrue/False以外の値が残ってしまう場合、置換処理は行われません。

df = pd.read_csv('sample.csv', true_values=['東京'])
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

skipinitialspace

区切り文字の末尾がスペースの場合、スペースをスキップするかどうかを指定します。デフォルトはFalse(スキップしない)です。

以下のように、カンマの直後にスペースが挿入されているCSVファイルを用意します。

$ cat sample_initialspace.csv
Date, Area, MinTemp, MaxTemp, WindSpeed, Rainfall
2023/4/1, 東京, 10.3, 23.3, 2.6, null
2023/4/1, 大阪, 10.4, 25.1, 2.3, null
2023/4/2, 東京, 11.1, 19.0, 3.2, 0
2023/4/2, 大阪, 10.9, 23.3, 4.4, null

デフォルト(skipinitialspace=False)では、カラムや値の先頭にスペースがそのまま付与されてしまいます。

df = pd.read_csv('sample_initialspace.csv')
print(list(df.columns))
['Date', ' Area', ' MinTemp', ' MaxTemp', ' WindSpeed', ' Rainfall']

skipinitialspace=Trueと指定することで、先頭のスペースを除去することができます。

df = pd.read_csv('sample_initialspace.csv', skipinitialspace=True)
print(list(df.columns))
['Date', 'Area', 'MinTemp', 'MaxTemp', 'WindSpeed', 'Rainfall']

skiprows

読み飛ばす行番号(0始まり)を指定します。デフォルトはNoneです。

整数のリストを指定すると、リストで指定された行番号のみがスキップされます。

df = pd.read_csv('sample.csv', skiprows=[1])
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  大阪     10.4     25.1        2.3       NaN
1  2023/4/2  東京     11.1     19.0        3.2       0.0
2  2023/4/2  大阪     10.9     23.3        4.4       NaN

整数Nを指定すると、ファイルの先頭N行(ヘッダー行を含む)がスキップされます。

df = pd.read_csv('sample.csv', skiprows=2, header=None)
print(df)
          0     1     2     3    4    5
0  2023/4/1  大阪  10.4  25.1  2.3  NaN
1  2023/4/2  東京  11.1  19.0  3.2  0.0
2  2023/4/2  大阪  10.9  23.3  4.4  NaN

skiprowsに関数を指定すると、関数がTrue(もしくはTrueと評価されるオブジェクト)を返すレコードのみが読み込まれます。

以下は、ファイルの偶数行のみを読み込む例です。

skiprows = lambda n: n % 2
df = pd.read_csv('sample.csv', skiprows=skiprows)
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  大阪     10.4     25.1        2.3       NaN
1  2023/4/2  大阪     10.9     23.3        4.4       NaN

skiprowsheaderを同時に指定する場合、skiprowsheaderの順に実行される点に注意が必要です。この場合、headerで指定する行番号は、ファイルの行番号ではなく、skiprowsによりスキップされた後の行番号を指定 します。

sample_multiheader.csvのデータ内容を再掲の上、skiprowsheaderを同時に指定してみます。

$ cat sample_multiheader.csv
# comment
Date,Area,MinTemp,MaxTemp,WindSpeed,Rainfall
# comment
年月日,観測地点,最低気温,最高気温,風速,降水量
2023/4/1,東京,10.3,23.3,2.6,null
2023/4/1,大阪,10.4,25.1,2.3,null
2023/4/2,東京,11.1,19.0,3.2,0
2023/4/2,大阪,10.9,23.3,4.4,null
df = pd.read_csv('sample_multiheader.csv', 
                 skiprows=[0, 2], header=[0, 1])
print(df)
       Date     Area  MinTemp  MaxTemp WindSpeed Rainfall
     年月日 観測地点 最低気温 最高気温      風速   降水量
0  2023/4/1     東京     10.3     23.3       2.6      NaN
1  2023/4/1     大阪     10.4     25.1       2.3      NaN
2  2023/4/2     東京     11.1     19.0       3.2      0.0
3  2023/4/2     大阪     10.9     23.3       4.4      NaN

skipfooter

フッター行数を整数で指定します。デフォルトは0です。

指定すると、ファイル末尾のフッターレコードがスキップされます。skipfooterは、engine='python'でのみ有効です。

$ cat sample_footer.csv
Date,Area,MinTemp,MaxTemp,WindSpeed,Rainfall
2023/4/1,東京,10.3,23.3,2.6,null
2023/4/1,大阪,10.4,25.1,2.3,null
2023/4/2,東京,11.1,19.0,3.2,0
2023/4/2,大阪,10.9,23.3,4.4,null
# footer
df = pd.read_csv('sample_footer.csv', skipfooter=1,
                 engine='python')
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

nrows

読み込むレコード数(ヘッダー行数を除く)を指定します。

データ分析等で大きなCSVファイルの中身をざっと確認したい時、最初にnrowsでファイルの先頭部分のみを読み込んでデータ内容を確認するのに重宝します。

df = pd.read_csv('sample.csv', nrows=2)
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN

low_memory

データ型の推定方法を指定します。

デフォルトはTrueで、ファイルを部分的に読み込みながらデータ型を推定します。一度に全データを使用しないため、メモリ消費量を抑える効果がありますが、推定されたデータ型と異なる値が後から読み込まれる可能性があります(その場合、以下のようにDtypeWarningが送出されます)。

# 最初の100万行は数字、末尾の1行だけ文字列のデータを生成して読み込む
with StringIO('\n'.join('x' + '0'*1000000 + 'a')) as f:
    df = pd.read_csv(f)

print(df.dtypes)
x    object
dtype: object


/tmp/ipykernel_6667/2567239896.py:3: DtypeWarning: Columns (0) have mixed types. Specify dtype option on import or set low_memory=False.
  df = pd.read_csv(f)

一方、low_memory=Falseとすると、データ全体を読み込んでからデータ型が推定されます。メモリ消費量は増加しますが、正確なデータ型を特定することが可能です。

with StringIO('\n'.join('x' + '0'*1000000 + 'a')) as f:
    df = pd.read_csv(f, low_memory=False)
    
print(df.dtypes)
x    object
dtype: object

memory_map

ファイルオブジェクトをメモリに直接マッピングするかどうかを指定します。デフォルトはFalseで、filepath_or_buffer引数にファイルパスが指定された場合にのみ有効です(io.StringIO等のメモリ上にあるデータは対象外)。

Trueに設定するとディスクI/Oのオーバーヘッドがなくなり、パフォーマンスが向上することがあります。

効果を確認するため、Amazon Customer Reviews Datasetから日本語のAmazonレビューデータをダウンロードして使用します。データ件数は約260万行です。

$ wget -q https://s3.amazonaws.com/amazon-reviews-pds/tsv/amazon_reviews_multilingual_JP_v1_00.tsv.gz
$ gzip -df amazon_reviews_multilingual_JP_v1_00.tsv.gz
$ wc -l amazon_reviews_multilingual_JP_v1_00.tsv
262432 amazon_reviews_multilingual_JP_v1_00.tsv

読み込み時間を計測すると、平均値ではmemory_map=Trueが若干早いようですが、標準偏差も含めると明確な変化は見られませんでした。

filename = 'amazon_reviews_multilingual_JP_v1_00.tsv'

%timeit pd.read_csv(filename, sep='\t', encoding='utf-8', memory_map=False)
%timeit pd.read_csv(filename, sep='\t', encoding='utf-8', memory_map=True)
2.59 s ± 226 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
2.47 s ± 37.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

欠損値(NA/NaN)のハンドリング

na_values

欠損値とみなす値を指定します。デフォルトはNoneです。

pandasでは以下の文字列群が「欠損値と見なす値」として事前定義されています。本記事ではこれを「欠損値リスト」と呼ぶことにします。

print(pd._libs.parsers.STR_NA_VALUES)
{'', 'nan', 'NaN', '<NA>', '-1.#QNAN', '-NaN', 'n/a', 'NA', 'null', 'NULL', 'N/A', '#N/A N/A', '#N/A', '1.#IND', '-1.#IND', '#NA', '1.#QNAN', '-nan', 'None'}

na_valuesに単一の値を指定すると、指定値が欠損値に置換されます。

df = pd.read_csv('sample.csv', na_values=0.0)
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       NaN
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

リストを指定すると、リスト内の値が欠損値に置換されます。

df = pd.read_csv('sample.csv', na_values=['東京', 0.0])
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1   NaN     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2   NaN     11.1     19.0        3.2       NaN
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

辞書を指定すると、カラム毎に異なる欠損値を指定することができます。

df = pd.read_csv('sample.csv', 
                 na_values={'Area': '東京', 'MinTemp': [10.4, 10.9]})
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1   NaN     10.3     23.3        2.6       NaN
1  2023/4/1  大阪      NaN     25.1        2.3       NaN
2  2023/4/2   NaN     11.1     19.0        3.2       0.0
3  2023/4/2  大阪      NaN     23.3        4.4       NaN

keep_default_na

欠損値リストを使用するかどうかを指定します。デフォルトはTrue(使用する)です。

ほとんどの値が欠損値のファイルを作成して検証します。

$ cat sample_nan.csv
A,B,C,D,E
1,2,3,4,5
,N/A,NULL,-nan,None
1.#IND,NaN,-1.#QNAN,<NA>,null
-NaN,#N/A N/A,1.#QNAN,-1.#IND,#N/A
NA,nan,#NA,n/a,

デフォルト(keep_default_na=True, na_values=None)では、欠損値リストの値のみが欠損値に自動変換されます。

df = pd.read_csv('sample_nan.csv')
print(df)
     A    B    C    D    E
0  1.0  2.0  3.0  4.0  5.0
1  NaN  NaN  NaN  NaN  NaN
2  NaN  NaN  NaN  NaN  NaN
3  NaN  NaN  NaN  NaN  NaN
4  NaN  NaN  NaN  NaN  NaN

na_values引数を指定すると、指定した値も併せて欠損値に置換されます。

df = pd.read_csv('sample_nan.csv', na_values=[2, 4])
print(df)
     A   B    C   D    E
0  1.0 NaN  3.0 NaN  5.0
1  NaN NaN  NaN NaN  NaN
2  NaN NaN  NaN NaN  NaN
3  NaN NaN  NaN NaN  NaN
4  NaN NaN  NaN NaN  NaN

keep_default_na=Falseにすると、欠損値リストの値は欠損値に置換されません。

df = pd.read_csv('sample_nan.csv', keep_default_na=False)
print(df)
        A         B         C        D     E
0       1         2         3        4     5
1               N/A      NULL     -nan  None
2  1.#IND       NaN  -1.#QNAN     <NA>  null
3    -NaN  #N/A N/A   1.#QNAN  -1.#IND  #N/A
4      NA       nan       #NA      n/a      

keep_default_na=Falseの状態でna_valuesを指定すると、指定値のみが欠損値に置換されます。

df = pd.read_csv('sample_nan.csv', keep_default_na=False, 
                 na_values=[2, 4])
print(df)
        A         B         C        D     E
0       1       NaN         3      NaN     5
1               N/A      NULL     -nan  None
2  1.#IND       NaN  -1.#QNAN     <NA>  null
3    -NaN  #N/A N/A   1.#QNAN  -1.#IND  #N/A
4      NA       nan       #NA      n/a      

na_filter

欠損値の置換処理を有効化するかどうかを指定します。デフォルトはTrue(有効)です。

Falseにすると置換処理は無効化されます。

無効化するとファイル読み込みのパフォーマンスが向上しますので、前処理済みデータのように「欠損値が含まれていないことが担保されている」CSVファイルには効果的です。

df = pd.read_csv('sample_nan.csv', na_filter=False)
print(df)
        A         B         C        D     E
0       1         2         3        4     5
1               N/A      NULL     -nan  None
2  1.#IND       NaN  -1.#QNAN     <NA>  null
3    -NaN  #N/A N/A   1.#QNAN  -1.#IND  #N/A
4      NA       nan       #NA      n/a      

なお、na_filter=Falseにすると、keep_default_nana_valuesは無視されます。警告も出力されません。

df = pd.read_csv('sample_nan.csv', na_filter=False, 
                 keep_default_na=True, na_values=[2, 4])
print(df)
        A         B         C        D     E
0       1         2         3        4     5
1               N/A      NULL     -nan  None
2  1.#IND       NaN  -1.#QNAN     <NA>  null
3    -NaN  #N/A N/A   1.#QNAN  -1.#IND  #N/A
4      NA       nan       #NA      n/a      

verbose

欠損値の処理に要した時間が出力されます。デフォルトはFalseです。

CSVファイルの読み込みが遅い場合にverbose=Trueとすることで、欠損値処理のオーバーヘッドをデバッグするのに役立ちそうです。

df = pd.read_csv('sample_nan.csv', verbose=True)
Tokenization took: 0.01 ms
Type conversion took: 0.25 ms
Parser memory cleanup took: 0.00 ms

skip_blank_lines

空行をスキップするかどうかを指定します。デフォルトはTrue(スキップする)です。

空行を含む以下ファイルで検証します。

$ cat sample_blankline.csv
Date,Area,MinTemp,MaxTemp,WindSpeed,Rainfall

2023/4/1,東京,10.3,23.3,2.6,null
2023/4/1,大阪,10.4,25.1,2.3,null

2023/4/2,東京,11.1,19.0,3.2,0
2023/4/2,大阪,10.9,23.3,4.4,null
df = pd.read_csv('sample_blankline.csv')
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

Falseとすると、空行は全て欠損値として扱われます。

df = pd.read_csv('sample_blankline.csv', skip_blank_lines=False)
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0       NaN   NaN      NaN      NaN        NaN       NaN
1  2023/4/1  東京     10.3     23.3        2.6       NaN
2  2023/4/1  大阪     10.4     25.1        2.3       NaN
3       NaN   NaN      NaN      NaN        NaN       NaN
4  2023/4/2  東京     11.1     19.0        3.2       0.0
5  2023/4/2  大阪     10.9     23.3        4.4       NaN

日付・時刻のハンドリング

CSVファイルの文字列から日付・時刻に変換する引数を解説します。

様々なフォーマットで日付・時刻を表現した以下サンプルファイルで検証します。各カラムは以下の通りです。

  • iso8601: 国際規格であるISO8601による表記
  • year/month/day/time: 年/月/日/時刻を各カラムに分割
  • dayfirst: イギリス式と呼ばれる、DD/MM/YYYY(先頭が日)による表記
  • japanese: 日本語による表記
$ cat sample_datetime.csv
iso8601,year,month,day,time,dayfirst,japanese
2023-04-01T00:00:00,2023,5,1,00:00:00,1/6/2023 00:00:00,2023年7月1日 00時00分00秒
2023-04-02T12:34:56,2023,5,2,12:34:56,2/6/2023 12:34:56,2023年7月2日 12時34分56秒
2023-04-03T23:59:59,2023,5,3,23:59:59,3/6/2023 23:59:59,2023年7月3日 23時59分59秒

デフォルトの引数では、どのカラムもdatetime型ではなく、objectもしくはint64型で読み込まれます。

df = pd.read_csv('sample_datetime.csv')
print(df)
               iso8601  year  month  day      time           dayfirst                   japanese
0  2023-04-01T00:00:00  2023      5    1  00:00:00  1/6/2023 00:00:00  2023年7月1日 00時00分00秒
1  2023-04-02T12:34:56  2023      5    2  12:34:56  2/6/2023 12:34:56  2023年7月2日 12時34分56秒
2  2023-04-03T23:59:59  2023      5    3  23:59:59  3/6/2023 23:59:59  2023年7月3日 23時59分59秒

parse_dates

日付とするカラムを指定します。デフォルトはNoneです。

カラム名か列番号のリストを指定すると、該当カラムがnumpy.datetime64に変換されます。

df = pd.read_csv('sample_datetime.csv', parse_dates=['iso8601'])
print(df)
              iso8601  year  month  day      time           dayfirst                   japanese
0 2023-04-01 00:00:00  2023      5    1  00:00:00  1/6/2023 00:00:00  2023年7月1日 00時00分00秒
1 2023-04-02 12:34:56  2023      5    2  12:34:56  2/6/2023 12:34:56  2023年7月2日 12時34分56秒
2 2023-04-03 23:59:59  2023      5    3  23:59:59  3/6/2023 23:59:59  2023年7月3日 23時59分59秒

日付列をindex_colで指定してparse_dates=Trueとすると、インデックスがdatetimeに変換されます。

df = pd.read_csv('sample_datetime.csv', index_col='iso8601', parse_dates=True)
print(df)
                     year  month  day      time           dayfirst                   japanese
iso8601                                                                                      
2023-04-01 00:00:00  2023      5    1  00:00:00  1/6/2023 00:00:00  2023年7月1日 00時00分00秒
2023-04-02 12:34:56  2023      5    2  12:34:56  2/6/2023 12:34:56  2023年7月2日 12時34分56秒
2023-04-03 23:59:59  2023      5    3  23:59:59  3/6/2023 23:59:59  2023年7月3日 23時59分59秒

2重リストで指定すると、複数のカラムを単一のdatetimeカラムに変換できます。

year/month/dayの各カラムを1つの日付に変換するには、以下のようにします。変換後のカラム名は、元のカラム名を_で結合したものです。

df = pd.read_csv('sample_datetime.csv', parse_dates=[['year', 'month', 'day']])
print(df)
  year_month_day              iso8601      time           dayfirst                   japanese
0     2023-05-01  2023-04-01T00:00:00  00:00:00  1/6/2023 00:00:00  2023年7月1日 00時00分00秒
1     2023-05-02  2023-04-02T12:34:56  12:34:56  2/6/2023 12:34:56  2023年7月2日 12時34分56秒
2     2023-05-03  2023-04-03T23:59:59  23:59:59  3/6/2023 23:59:59  2023年7月3日 23時59分59秒

複数カラムの変換結果は、DataFrame0カラム目に挿入されます。入力ファイルのカラム順と一致しなくなりますのでご注意下さい。

日付と時刻を結合することも可能です。

df = pd.read_csv('sample_datetime.csv', parse_dates=[['year', 'month', 'day', 'time']])
print(df)
  year_month_day_time              iso8601           dayfirst                   japanese
0 2023-05-01 00:00:00  2023-04-01T00:00:00  1/6/2023 00:00:00  2023年7月1日 00時00分00秒
1 2023-05-02 12:34:56  2023-04-02T12:34:56  2/6/2023 12:34:56  2023年7月2日 12時34分56秒
2 2023-05-03 23:59:59  2023-04-03T23:59:59  3/6/2023 23:59:59  2023年7月3日 23時59分59秒

parse_datesが日付推定できなかった場合、dateutil.parser.parseパーサーにフォールバックして再度推定されます。

df = pd.read_csv('sample_datetime.csv', parse_dates=[['year', 'month']])
print(df)
  year_month              iso8601  day      time           dayfirst                   japanese
0 2023-05-01  2023-04-01T00:00:00    1  00:00:00  1/6/2023 00:00:00  2023年7月1日 00時00分00秒
1 2023-05-01  2023-04-02T12:34:56    2  12:34:56  2/6/2023 12:34:56  2023年7月2日 12時34分56秒
2 2023-05-01  2023-04-03T23:59:59    3  23:59:59  3/6/2023 23:59:59  2023年7月3日 23時59分59秒


/tmp/ipykernel_6667/2411069377.py:1: UserWarning: Could not infer format, so each element will be parsed individually, falling back to `dateutil`. To ensure parsing is consistent and as-expected, please specify a format.
  df = pd.read_csv('sample_datetime.csv', parse_dates=[['year', 'month']])

辞書を指定すれば、変換後のカラム名を任意の名前に変更できます。

df = pd.read_csv('sample_datetime.csv', 
                 parse_dates={'Date': ['year', 'month', 'day'], 
                              'Datetime': ['year', 'month', 'day', 'time']})
print(df)
        Date            Datetime              iso8601           dayfirst                   japanese
0 2023-05-01 2023-05-01 00:00:00  2023-04-01T00:00:00  1/6/2023 00:00:00  2023年7月1日 00時00分00秒
1 2023-05-02 2023-05-02 12:34:56  2023-04-02T12:34:56  2/6/2023 12:34:56  2023年7月2日 12時34分56秒
2 2023-05-03 2023-05-03 23:59:59  2023-04-03T23:59:59  3/6/2023 23:59:59  2023年7月3日 23時59分59秒

infer_datetime_format / date_parser

どちらの引数もver2.0で非推奨となりました。代わりに、同バージョンで新たに追加されたdate_format引数を使用して下さい。

keep_date_col

parse_datesで日付変換する場合、変換前のカラムを残すかどうかを指定します。デフォルトはFalse(残さない)です。

Trueに設定すると、変換前のカラムは削除されません。

df = pd.read_csv('sample_datetime.csv', parse_dates=[['year', 'month', 'day']], 
                 keep_date_col=True)
print(df)
  year_month_day              iso8601  year month day      time           dayfirst                   japanese
0     2023-05-01  2023-04-01T00:00:00  2023     5   1  00:00:00  1/6/2023 00:00:00  2023年7月1日 00時00分00秒
1     2023-05-02  2023-04-02T12:34:56  2023     5   2  12:34:56  2/6/2023 12:34:56  2023年7月2日 12時34分56秒
2     2023-05-03  2023-04-03T23:59:59  2023     5   3  23:59:59  3/6/2023 23:59:59  2023年7月3日 23時59分59秒

dayfirst

DD/MM/YYYYな日付フォーマットの変換有無を指定します。デフォルトはFalse(変換しない)です。

サンプルデータのdayfirstカラムを日付変換すると、デフォルトではMM/DD/YYYYで変換されてしまい、月と日が逆転してしまいます。

df = pd.read_csv('sample_datetime.csv', parse_dates=['dayfirst'])
print(df)
               iso8601  year  month  day      time            dayfirst                   japanese
0  2023-04-01T00:00:00  2023      5    1  00:00:00 2023-01-06 00:00:00  2023年7月1日 00時00分00秒
1  2023-04-02T12:34:56  2023      5    2  12:34:56 2023-02-06 12:34:56  2023年7月2日 12時34分56秒
2  2023-04-03T23:59:59  2023      5    3  23:59:59 2023-03-06 23:59:59  2023年7月3日 23時59分59秒

Trueに指定することで、正しい日付で変換されます。

df = pd.read_csv('sample_datetime.csv', parse_dates=['dayfirst'], dayfirst=True)
print(df)
               iso8601  year  month  day      time            dayfirst                   japanese
0  2023-04-01T00:00:00  2023      5    1  00:00:00 2023-06-01 00:00:00  2023年7月1日 00時00分00秒
1  2023-04-02T12:34:56  2023      5    2  12:34:56 2023-06-02 12:34:56  2023年7月2日 12時34分56秒
2  2023-04-03T23:59:59  2023      5    3  23:59:59 2023-06-03 23:59:59  2023年7月3日 23時59分59秒

date_format

日付フォーマットを文字列で指定します。ver2.0で新たに追加されました。デフォルトはNoneです。

指定したフォーマットに従って日付変換が実行されます。この引数は、parse_datesと併せて使用します。

日本語で表記されたjapaneseカラムも、正しい日付で変換できます。

df = pd.read_csv('sample_datetime.csv', parse_dates=['japanese'], 
                 date_format='%Y年%m月%d日 %H時%M分%S秒')
print(df)
               iso8601  year  month  day      time           dayfirst            japanese
0  2023-04-01T00:00:00  2023      5    1  00:00:00  1/6/2023 00:00:00 2023-07-01 00:00:00
1  2023-04-02T12:34:56  2023      5    2  12:34:56  2/6/2023 12:34:56 2023-07-02 12:34:56
2  2023-04-03T23:59:59  2023      5    3  23:59:59  3/6/2023 23:59:59 2023-07-03 23:59:59

date_formatには任意の書式の他、ISO8601mixedといった指定も可能です。

ここまで解説した引数を活用し、サンプルデータの全カラムをdatetimeに変換するには以下のようにします。dayfirst=Trueが全てのカラムに適用されてしまうため、datetime1datetime2には明示的にISO8601と指定しています。

df = pd.read_csv('sample_datetime.csv', dayfirst=True,
                 parse_dates={
                     'datetime1': ['iso8601'],
                     'datetime2': ['year', 'month', 'day', 'time'],
                     'datetime3': ['dayfirst'],
                     'datetime4': ['japanese']
                 },
                 date_format={
                     'datetime1': 'ISO8601',
                     'datetime2': 'ISO8601',
                     'datetime4': '%Y年%m月%d日 %H時%M分%S秒'
                 })
print(df)
            datetime1           datetime2           datetime3           datetime4
0 2023-04-01 00:00:00 2023-05-01 00:00:00 2023-06-01 00:00:00 2023-07-01 00:00:00
1 2023-04-02 12:34:56 2023-05-02 12:34:56 2023-06-02 12:34:56 2023-07-02 12:34:56
2 2023-04-03 23:59:59 2023-05-03 23:59:59 2023-06-03 23:59:59 2023-07-03 23:59:59

cache_dates

日付の変換結果をキャッシュするかどうかを指定します。デフォルトはTrue(キャッシュする)です。

Falseでキャッシュを無効化できます。

df = pd.read_csv('sample_datetime.csv', parse_dates=['iso8601'], cache_dates=False)
print(df)
              iso8601  year  month  day      time           dayfirst                   japanese
0 2023-04-01 00:00:00  2023      5    1  00:00:00  1/6/2023 00:00:00  2023年7月1日 00時00分00秒
1 2023-04-02 12:34:56  2023      5    2  12:34:56  2/6/2023 12:34:56  2023年7月2日 12時34分56秒
2 2023-04-03 23:59:59  2023      5    3  23:59:59  3/6/2023 23:59:59  2023年7月3日 23時59分59秒

重複する日付がファイルに多数含まれている場合、キャッシュされた変換結果を再利用することで、読み込み時間を短縮できます。キャッシュ効果の検証結果は次章をご参照下さい。

イテレーション

CSVファイルの分割読み込みを指定します。

最初の気象サンプルデータを再掲します。

$ cat sample.csv
Date,Area,MinTemp,MaxTemp,WindSpeed,Rainfall
2023/4/1,東京,10.3,23.3,2.6,null
2023/4/1,大阪,10.4,25.1,2.3,null
2023/4/2,東京,11.1,19.0,3.2,0
2023/4/2,大阪,10.9,23.3,4.4,null

iterator / chunksize

CSVファイルの分割読み込みを指定します。iteratorのデフォルトはFalse(分割しない)です。

Trueに設定すると、ファイル全体を一度に読み込まず、代わりにTextFileReaderというイテレータを返します。

reader = pd.read_csv('sample.csv', iterator=True)
print(type(reader))
<class 'pandas.io.parsers.readers.TextFileReader'>

ループ処理と組み合わせることで、TextFileReaderからDataFrameを順次取得することができます。chunksizeに整数を指定することで、指定行数ずつ順に読み込まれます。

reader = pd.read_csv('sample.csv', iterator=True, chunksize=2)
for df in reader:
    print(df)
    print('-'*60)
reader.close()
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
------------------------------------------------------------
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN
------------------------------------------------------------

コンテキストマネージャーも使用できます。イテレーション処理の成功・失敗に関わらずTextFileReaderを必ずcloseしたい場合に便利です。

with pd.read_csv('sample.csv', iterator=True, chunksize=3) as reader:
    for df in reader:
        print(df)
        print('-'*60)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
------------------------------------------------------------
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
3  2023/4/2  大阪     10.9     23.3        4.4       NaN
------------------------------------------------------------

読み込む行数を固定せず変動させたい時は、TextFileReaderget_chunkという関数を使います。get_chunkに指定した行数が順次読み込まれます。

reader = pd.read_csv('sample.csv', iterator=True)
print(reader.get_chunk(1))
print('-'*60)
print(reader.get_chunk(3))
reader.close()
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
------------------------------------------------------------
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

このように、iteratorchunksizeは一度にメモリに収まらないような巨大なCSVファイルを分割処理したいときに便利です!

圧縮フォーマット、クォート、文字コードなど

CSVファイルの圧縮フォーマットや文字コード、クォート文字のパース方法などを指定します。

compression

ファイルの圧縮フォーマットを指定します。対応済みの圧縮フォーマットは以下の通りです。Zstdはver1.4、Tarはver1.5でそれぞれ追加されました。

print(pd.io.common._supported_compressions)
{'xz', 'bz2', 'tar', 'gzip', 'zip', 'zstd'}

compressionのデフォルトはinferです。ファイル拡張子から自動的に圧縮フォーマットが推定され、オンザフライで解凍して読み込まれます。

$ gzip -kf sample.csv
df = pd.read_csv('sample.csv.gz')
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

compression='gzip'のように明示的に指定すると、ファイル拡張子に関係なく指定された圧縮フォーマットで解凍されて読み込まれます。Noneとすれば、解凍せずにテキストファイルとして読み込まれます。

df = pd.read_csv('sample.csv.gz', compression='gzip')
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

thousands

桁区切りの文字を指定します。以下のファイルで検証します。

$ cat sample_thousands.csv
product,price
pizza,"2,980"
pc,"238,000"
car,"4,540,000"

thousandsを指定しない場合、クォーテーション内のカンマは文字列として扱われるため、priceカラムのデータ型はobject型となります。

df = pd.read_csv('sample_thousands.csv')
print(df.dtypes)
product    object
price      object
dtype: object

thousands=','と指定することで、priceカラムを数値として読み込むことができます。

df = pd.read_csv('sample_thousands.csv', thousands=',')
print(df.dtypes)
product    object
price       int64
dtype: object

decimal

小数点とする文字を指定します。デフォルトは.です。

国際的には、小数点にはピリオドとカンマのいずれかを使用する国が多いようです(Wikipedia(小数点)より引用)。

小数点として点(point on the line)を用いるか、コンマ(comma on the line)を用いるかは、国、地域、文化によってまちまちである。ごく大まかには、イギリス、米国、日本、中国、インドでは「点」を用い、フランス、ドイツ、イタリア、スペイン、ロシアでは「コンマ」を用いる。以下では、国際度量衡総会での呼び方にならって、「点」を用いる方式を「イギリス式(British practice)」、「コンマ」を用いる方式を「フランス式(French practice)」と呼称する。

以下、カンマを小数点とするフランス式のファイルで検証します。

$ cat sample_french.csv
Date,Area,MinTemp,MaxTemp,WindSpeed,Rainfall
2023/4/1,東京,"10,3","23,3","2,6",
2023/4/1,大阪,"10,4","25,1","2,3",
2023/4/2,東京,"11,1","19,0","3,2","0,0"
2023/4/2,大阪,"10,9","23,3","4,4",
df = pd.read_csv('sample_french.csv', decimal=',')
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

float_precision

浮動小数点数のコンバーターを指定します。デフォルトはNoneです。

コンバーターにはNone(通常)、high(高精度)、legacy(低精度)、round_trip(ラウンドトリップ)のいずれかを指定できます。

桁数の多い浮動小数点数のファイルを準備し、各コンバーターで読み込んでみます。

$ cat sample_precision.csv
value
3.141592653589793238462643383279502884
df_none = pd.read_csv('sample_precision.csv', float_precision=None)
df_high = pd.read_csv('sample_precision.csv', float_precision='high')
df_legacy = pd.read_csv('sample_precision.csv', float_precision='legacy')
df_round_trip = pd.read_csv('sample_precision.csv', float_precision='round_trip')

ファイルと同じ値で差分すると、Nonehighでは丸め誤差による僅かな値が現れたのに対し、legacyround_tripは正確に0となりました。

pythonpandasはどちらもfloatに倍精度浮動小数点数(64bit)を採用しているため、丸め誤差が影響を与えるようなユースケースは少ないかと思いますが、厳密な算術結果が求められる場面ではround_tripが有益となりそうです。

value = 3.141592653589793238462643383279502884

print(df_none - value)
print('-'*15)
print(df_high - value)
print('-'*15)
print(df_legacy - value)
print('-'*15)
print(df_round_trip - value)
          value
0 -4.440892e-16
---------------
          value
0 -4.440892e-16
---------------
   value
0    0.0
---------------
   value
0    0.0

lineterminator

改行コード(任意の1文字)を指定します。デフォルトは実行環境に依存します。

MacOS9以前で使用されていたCR改行コードのファイルを用意します。

$ cat -A sample_cr.csv
Date,Area,MinTemp,MaxTemp,WindSpeed,Rainfall^M2023/4/1,M-fM-^]M-1M-dM-:M-,,10.3,23.3,2.6,null^M2023/4/1,M-eM-$M-'M-iM-^XM-*,10.4,25.1,2.3,null^M2023/4/2,M-fM-^]M-1M-dM-:M-,,11.1,19.0,3.2,0^M2023/4/2,M-eM-$M-'M-iM-^XM-*,10.9,23.3,4.4,null^M

lineterminator='\r'とすることで、LFCRLF以外の改行コードでも読み込むことができます。

df = pd.read_csv('sample_cr.csv', lineterminator='\r')
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

lineterminatorengine='c'でのみ有効です。他のエンジンを明示的に指定するとValueErrorが送出されます。

try:
    df = pd.read_csv('sample_cr.csv', lineterminator='\r', 
                     engine='python')
except Exception as e:
    print(repr(e))
ValueError('Custom line terminators not supported in python parser (yet)')

quotechar

クォートに使用する文字を指定します。デフォルトは"で、任意の1文字を指定できます。

各フィールドをシングルクォーテーションで囲ったファイルで検証します。

$ cat sample_singlequote.csv
'Date','Area','MinTemp','MaxTemp','WindSpeed','Rainfall'
'2023/4/1','東京','10.3','23.3','2.6',''
'2023/4/1','大阪','10.4','25.1','2.3',''
'2023/4/2','東京','11.1','19.0','3.2','0.0'
'2023/4/2','大阪','10.9','23.3','4.4',''

quotecharを指定せずに読み込むと、シングルクォーテーションも値の一部として読み込まれます。

df = pd.read_csv('sample_singlequote.csv')
print(df)
       'Date'  'Area' 'MinTemp' 'MaxTemp' 'WindSpeed' 'Rainfall'
0  '2023/4/1'  '東京'    '10.3'    '23.3'       '2.6'         ''
1  '2023/4/1'  '大阪'    '10.4'    '25.1'       '2.3'         ''
2  '2023/4/2'  '東京'    '11.1'    '19.0'       '3.2'      '0.0'
3  '2023/4/2'  '大阪'    '10.9'    '23.3'       '4.4'         ''

quotechar='\''とすることで、シングルクォートがクォート文字として適切に処理されました。

df = pd.read_csv('sample_singlequote.csv', quotechar='\'')
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

quoting

クォートの動作を指定します。デフォルトはcsv.QUOTE_MINIMAL(0)です。

quotingには以下4つのパターンがあり、0~3の整数、もしくはcsvモジュールの各定数名で指定します。

指定値 クォートされる対象
0 もしくは csv.QUOTE_MINIMAL sepquotecharlineterminatorなどの文字列を含む値のみ
1 もしくは csv.QUOTE_ALL 全ての値
2 もしくは csv.QUOTE_NONNUMERIC 数値以外の値
3 もしくは csvQUOTE_NONE クォートしない

カンマを含む文字列をダブルクォートで囲ったファイルで検証します。

$ cat sample_quoting.csv
year,title,famous_scene
1596,Romeo and Juliet,"O Romeo, Romeo! wherefore art thou Romeo?"
1599,Julius Caesar,"And you, Brutus!"
1601,Hamlet,"To be, or not to be: that is the question."

quoting=0(csv.QUOTE_MINIMAL)の場合、各カラムは適切に読み込まれています。quoting=1(csv.QUOTE_ALL)でも同じ結果となります。

df = pd.read_csv('sample_quoting.csv', quoting=0)
print(df)
   year             title                                famous_scene
0  1596  Romeo and Juliet   O Romeo, Romeo! wherefore art thou Romeo?
1  1599     Julius Caesar                            And you, Brutus!
2  1601            Hamlet  To be, or not to be: that is the question.

quoting=2(csv.QUOTE_NONNUMERIC)とすると、yearカラムがfloat型になります。これはcsv.QUOTE_NONNUMERICの以下仕様に沿っています。

reader に対しては、クオートされていない全てのフィールドを float 型に変換するよう指示します。

df = pd.read_csv('sample_quoting.csv', quoting=2)
print(df)
     year             title                                famous_scene
0  1596.0  Romeo and Juliet   O Romeo, Romeo! wherefore art thou Romeo?
1  1599.0     Julius Caesar                            And you, Brutus!
2  1601.0            Hamlet  To be, or not to be: that is the question.

quoting=3(csvQUOTE_NONE)では、各カラムはクォート処理されないため、カラムと値の関係がずれてしまいます。また、ダブルクォーテーションも値に含まれたままです。

df = pd.read_csv('sample_quoting.csv', quoting=3)
print(df)
                  year     title                           famous_scene
1596  Romeo and Juliet  "O Romeo      Romeo! wherefore art thou Romeo?"
1599     Julius Caesar  "And you                               Brutus!"
1601            Hamlet    "To be   or not to be: that is the question."

doublequote

連続した2つのクォートを、単一のクォートとみなすかどうかを指定します。デフォルトはTrueです。

famous_sceneカラムの前後を3つのダブルクォートで囲ったファイルで検証します。

$ cat sample_doublequote.csv
year,title,famous_scene
1596,Romeo and Juliet,"""O Romeo, Romeo! wherefore art thou Romeo?"""
1599,Julius Caesar,"""And you, Brutus!"""
1601,Hamlet,"""To be, or not to be: that is the question."""

デフォルトのdoublequote=Trueでは、famous_sceneカラムの前後2つのダブルクォートが単一のダブルクォートと認識され、期待通りの結果が得られます。

df = pd.read_csv('sample_doublequote.csv', doublequote=True)
print(df)
   year             title                                  famous_scene
0  1596  Romeo and Juliet   "O Romeo, Romeo! wherefore art thou Romeo?"
1  1599     Julius Caesar                            "And you, Brutus!"
2  1601            Hamlet  "To be, or not to be: that is the question."

doublequote=Falseとすると、以下では"""O Romeo Romeo! wherefore art thou Romeo?""""""O Romeo Romeo! wherefore art thou Romeo?"""の2カラムとして読み込まれてしまい、カラムと値がずれてしまいます。

df = pd.read_csv('sample_doublequote.csv', doublequote=False)
print(df)
                  year     title                             famous_scene
1596  Romeo and Juliet  "O Romeo      Romeo! wherefore art thou Romeo?"""
1599     Julius Caesar  "And you                               Brutus!"""
1601            Hamlet    "To be   or not to be: that is the question."""

escapechar

エスケープする特殊文字(1文字)を指定します。デフォルトはNoneです。

一例として、escapechar=' 'とするとスペースがエスケープされ、空白を除去した文字列として読み込まれます。

df = pd.read_csv('sample_quoting.csv', escapechar=' ')
print(df)
   year           title                         famous_scene
0  1596  RomeoandJuliet  ORomeo,Romeo!whereforeartthouRomeo?
1  1599    JuliusCaesar                       Andyou,Brutus!
2  1601          Hamlet    Tobe,ornottobe:thatisthequestion.

comment

コメント行の開始文字(1文字)を指定します。デフォルトはNoneです。

指定すると、その開始文字より右側のフィールドはスキップされます。コメントの開始文字が共通している場合は、headerskipfotterを個別に指定するよりもcommentを指定した方が楽です。

$ cat sample_comment.csv
# header
Date,Area,MinTemp,MaxTemp,WindSpeed,Rainfall  # comment
2023/4/1,東京,10.3,23.3,2.6,null  # comment
2023/4/1,大阪,10.4,25.1,2.3,null  # comment
2023/4/2,東京,11.1,19.0,3.2,0  # comment
2023/4/2,大阪,10.9,23.3,4.4,null  # comment
# footer
df = pd.read_csv('sample_header.csv', comment='#')
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

ファイル内のコメント開始文字が共通している場合は、headerskipfotterを個別に指定するよりもcommentで指定した方が楽です。

encoding

CSVファイルの文字コードを指定します。デフォルトはNoneです。

sample.csvを、Windowsで使用されているCP932という文字コードに変換したファイルで検証します。

$ iconv -f UTF-8 -t CP932 sample.csv > sample_cp932.csv
$ cat sample_cp932.csv | iconv -f CP932
Date,Area,MinTemp,MaxTemp,WindSpeed,Rainfall
2023/4/1,東京,10.3,23.3,2.6,null
2023/4/1,大阪,10.4,25.1,2.3,null
2023/4/2,東京,11.1,19.0,3.2,0
2023/4/2,大阪,10.9,23.3,4.4,null

上記ファイルをcp932で読み込みます。encodingに指定可能な文字コードの一覧は、Python公式ドキュメントの標準エンコーディングをご参照下さい。

df = pd.read_csv('sample_cp932.csv', encoding='cp932')
print(df)
       Date Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1   東京     10.3     23.3        2.6       NaN
1  2023/4/1   大阪     10.4     25.1        2.3       NaN
2  2023/4/2   東京     11.1     19.0        3.2       0.0
3  2023/4/2   大阪     10.9     23.3        4.4       NaN

ファイルの文字コードが不明な場合、chardetというPythonパッケージでファイルの文字コードを推定することができます。推定結果には、encodingに文字コード名、confidenceに確信度が出力されます。

あくまで推定値ですが、UTF/JIS/EUCといった文字コードの大体の種類を特定するのに役立ちます。

import chardet

for filename in ('sample.csv', 'sample_cp932.csv'):
    with open(filename, 'rb') as f:
        res = chardet.detect(f.read())
        print(f'{filename} : {res}')
sample.csv : {'encoding': 'utf-8', 'confidence': 0.99, 'language': ''}
sample_cp932.csv : {'encoding': 'Windows-1254', 'confidence': 0.3922507857706182, 'language': 'Turkish'}

encoding_errors

エンコードエラーが発生した場合の挙動を指定します。デフォルトはstrictで、UnicodeDecodeErrorが送出されます。

try:
    df = pd.read_csv('sample_sjis.csv', encoding='utf-8')
except UnicodeDecodeError as e:
    print(repr(e))
UnicodeDecodeError('utf-8', b'\x93\x8c\x8b\x9e', 0, 1, 'invalid start byte')

encoding_errors='ignore'とすると、エンコードに失敗した文字はスキップされます。

df = pd.read_csv('sample_sjis.csv', encoding='utf-8', encoding_errors='ignore')
print(df)
       Date Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1          10.3     23.3        2.6       NaN
1  2023/4/1          10.4     25.1        2.3       NaN
2  2023/4/2          11.1     19.0        3.2       0.0
3  2023/4/2          10.9     23.3        4.4       NaN

encoding_errors='replace'とすると、エンコードに失敗した文字はというUnicodeのREPLACEMENT CHARACTER (U+FFFD)に置換されます。

df = pd.read_csv('sample_sjis.csv', encoding_errors='replace')
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  ����     10.3     23.3        2.6       NaN
1  2023/4/1   ���     10.4     25.1        2.3       NaN
2  2023/4/2  ����     11.1     19.0        3.2       0.0
3  2023/4/2   ���     10.9     23.3        4.4       NaN

encoding_errors='backslashreplace'とすると、エンコードに失敗した文字はバックスラッシュによるエスケープシーケンスに置換されます。

df = pd.read_csv('sample_sjis.csv', encoding_errors='backslashreplace')
print(df)
       Date              Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  \x93\x8c\x8b\x9e     10.3     23.3        2.6       NaN
1  2023/4/1  \x91\xe5\x8d\xe3     10.4     25.1        2.3       NaN
2  2023/4/2  \x93\x8c\x8b\x9e     11.1     19.0        3.2       0.0
3  2023/4/2  \x91\xe5\x8d\xe3     10.9     23.3        4.4       NaN

dialect

CSVの細かな記法を指定します。デフォルトはNoneです。

本記事の冒頭で述べた通り、CSVは国際標準化される前から広く利用されてきたため、区切り文字やクォートの有無など、微妙な記法の違いが存在します。Pythonにはそれらの記法を定義できるcsv.Dialectというクラスが存在し、read_csvにも同クラスを適用することができます。

検証用に、csv.Dialectで定義されているexcelexcel-tabunixの各記法でCSVファイルを生成してみます。

import csv

with open('sample.csv') as f:
    reader = csv.DictReader(f)
    rows = [row for row in reader]
    fieldnames = reader.fieldnames

for dialect in ('excel', 'excel-tab', 'unix'):
    with open(f'sample_{dialect}.csv', 'w') as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames, dialect=dialect)
        writer.writeheader()
        writer.writerows(rows)

生成結果を比較してみましょう。

dialect='excel'では、改行コードがCRLF(レコード末尾の^M$)であり、クォートは必須ではないようです。

$ cat -A sample_excel.csv
Date,Area,MinTemp,MaxTemp,WindSpeed,Rainfall^M$
2023/4/1,M-fM-^]M-1M-dM-:M-,,10.3,23.3,2.6,null^M$
2023/4/1,M-eM-$M-'M-iM-^XM-*,10.4,25.1,2.3,null^M$
2023/4/2,M-fM-^]M-1M-dM-:M-,,11.1,19.0,3.2,0^M$
2023/4/2,M-eM-$M-'M-iM-^XM-*,10.9,23.3,4.4,null^M$

dialect='excel-tab'では、区切り文字がタブ(^I)となっており、それ以外はexcelと同じです。

$ cat -A sample_excel-tab.csv
Date^IArea^IMinTemp^IMaxTemp^IWindSpeed^IRainfall^M$
2023/4/1^IM-fM-^]M-1M-dM-:M-,^I10.3^I23.3^I2.6^Inull^M$
2023/4/1^IM-eM-$M-'M-iM-^XM-*^I10.4^I25.1^I2.3^Inull^M$
2023/4/2^IM-fM-^]M-1M-dM-:M-,^I11.1^I19.0^I3.2^I0^M$
2023/4/2^IM-eM-$M-'M-iM-^XM-*^I10.9^I23.3^I4.4^Inull^M$

最後のdialect='unix'では、改行コードがLF(レコード末尾の$)であり、全フィールドがクォート処理されています。

$ cat -A sample_unix.csv
"Date","Area","MinTemp","MaxTemp","WindSpeed","Rainfall"$
"2023/4/1","M-fM-^]M-1M-dM-:M-,","10.3","23.3","2.6","null"$
"2023/4/1","M-eM-$M-'M-iM-^XM-*","10.4","25.1","2.3","null"$
"2023/4/2","M-fM-^]M-1M-dM-:M-,","11.1","19.0","3.2","0"$
"2023/4/2","M-eM-$M-'M-iM-^XM-*","10.9","23.3","4.4","null"$

excelunixはそれぞれ改行コードが異なっていますが、read_csvは代表的な記法をデフォルトで認識できるため、どちらも特別な指定なく読み込むことができます。

df_excel = pd.read_csv('sample_excel.csv')
df_unix = pd.read_csv('sample_unix.csv')

# 2つのdataframeを比較(中身が異なる時のみ、AssertionErrorが送出)
pd.testing.assert_frame_equal(df_excel, df_unix)

print(df_excel)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

excel-tabはタブ区切りのためsep='\t'でも読み込めますが、以下のようにdialectのクラスを直接指定することもできます。

df = pd.read_csv('sample_excel-tab.csv', dialect=csv.excel_tab)
print(df)
       Date  Area  MinTemp  MaxTemp  WindSpeed  Rainfall
0  2023/4/1  東京     10.3     23.3        2.6       NaN
1  2023/4/1  大阪     10.4     25.1        2.3       NaN
2  2023/4/2  東京     11.1     19.0        3.2       0.0
3  2023/4/2  大阪     10.9     23.3        4.4       NaN

もし今後、csv.dialectクラスにも定義されていない独自のCSV記法を扱う場面があれば、dialect引数が役立ちそうです。

エラーハンドリング

フィールド数がヘッダーのカラム数よりも多いレコード(以下、不正レコードと呼称)の扱いを指定します。デフォルトはerrorです。

不正レコードを含んだ以下ファイルで検証します。末尾2行が不正レコードです。

$ cat sample_badlines.csv
X,Y,Z
1,2,3
4,5,6,7
8,9,10,11,12

error_bad_lines / warn_bad_lines

どちらもver1.3で非推奨となり、ver2.0で削除されました。代わりに、ver1.3で追加されたon_bad_lines引数を使用して下さい。

on_bad_lines

不正レコードに対する処理方法を指定します。

デフォルトはerrorで、不正レコードが検知されると以下のようにParserErrorが送出され、読み込みが失敗します。

try:
    df = pd.read_csv('sample_badlines.csv')
except Exception as e:
    print(repr(e))
ParserError('Error tokenizing data. C error: Expected 3 fields in line 3, saw 4\n')

on_bad_lines='warn'にすると、不正レコードは警告が出力されてスキップされ、正常なレコードのみ読み込むことができます。

df = pd.read_csv('sample_badlines.csv', on_bad_lines='warn')
print(df)
   X  Y  Z
0  1  2  3


Skipping line 3: expected 3 fields, saw 4
Skipping line 4: expected 3 fields, saw 5

on_bad_lines='skip'にすると、不正レコードは警告無しでスキップされ、正常なレコードのみ読み込むことができます。

df = pd.read_csv('sample_badlines.csv', on_bad_lines='skip')
print(df)
   X  Y  Z
0  1  2  3

on_bad_linesには関数も指定できます。関数の引数には、sepで区切られた文字列のリストが入力されます。関数がNoneを返すとその行はスキップされ、関数が文字列のリストを返すとその返り値が読み込み結果となります。関数はengine='python'でのみ使用可能です。

# カラム数とフィールド数が不一致の場合、レコードの末尾3フィールドを返す
on_bad_lines = lambda line: line[-3:]
    
df = pd.read_csv('sample_badlines.csv', on_bad_lines=on_bad_lines,
                 engine='python')
print(df)
    X   Y   Z
0   1   2   3
1   5   6   7
2  10  11  12

Kaggle等のデータ分析コンペでは少数の不正レコードをskipで無視することがよくありますが、基幹業務では通常、検知された不正レコードを保存して後処理する必要があります。

以下のように関数内で不正レコードをリストに退避しておけば、正常なレコードのみを読み込みつつ、不正レコードに後処理を実行することが可能です。リスト退避以外にも、ファイルやログに出力するのにも使えます。

bad_lines = []
on_bad_lines = lambda line: bad_lines.append(line)
    
df = pd.read_csv('sample_badlines.csv', on_bad_lines=on_bad_lines,
                 engine='python')
print(df)
print('-' * 15)

print(f'{len(bad_lines)} bad lines found.')
print(bad_lines)
   X  Y  Z
0  1  2  3
---------------
2 bad lines found.
[['4', '5', '6', '7'], ['8', '9', '10', '11', '12']]

パフォーマンス比較

前章では、各引数の効果や制約を中心に検証しました。

本章では以下5つについて、実行時間とメモリ消費量がどれだけ違うのかパフォーマンスを比較評価していきます。

  • engineに指定するパーサーエンジンを、pythoncpyarrowで比較
  • dtypeの指定有無で比較
  • 数値や文字列の前処理において、読み込みと同時にconvertersで変換するパターンと、読み込み後に変換するパターンを比較
  • 日付変換において、読み込みと同時にparse_datesで変換するパターンと、読み込み後にpandas.to_datetimeで変換するパターンを比較
  • 日付変換におけるcache_datesの指定有無を比較

計測用に、データ型が異なる5カラム × 5,000万行のサンプルデータを作成します。

from string import ascii_lowercase 

N = 5 * 10**7
df = pd.DataFrame(index=pd.RangeIndex(0, N))
df['int'] = np.random.randint(low=0, high=256, size=N, dtype=np.uint8)
df['float'] = np.random.rand(N).astype(np.float32)
df['bool'] = df['float'] > 0.5
df['string'] = np.random.choice(list(ascii_lowercase), size=N)
df['datetime'] = pd.date_range(start='2023/4/1', periods=N, freq='min')
print(df.head())

filename = 'sample_large.csv'
df.to_csv(filename, index=False)
del df
   int     float   bool string            datetime
0  147  0.309425  False      w 2023-04-01 00:00:00
1  164  0.531861   True      j 2023-04-01 00:01:00
2  235  0.292943  False      s 2023-04-01 00:02:00
3  159  0.271452  False      g 2023-04-01 00:03:00
4  179  0.840848   True      e 2023-04-01 00:04:00

メモリ計測用にインストールしたmemory_profilerのnotebook拡張機能を有効化します。

%load_ext memory_profiler

1. engineのパフォーマンス比較

pythoncpyarrowの3つのパーサーエンジンで比較します。

読み込み時間では、pyarrowエンジンが圧倒的に速い結果となりました。cエンジンの約5倍、pythonエンジンの約20倍です!pyarrowはマルチスレッドに対応している唯一のエンジンであるため、vCPU数が多い環境ほど読み込み時間の短縮が期待できます。

%timeit pd.read_csv(filename, engine='python')
%timeit pd.read_csv(filename, engine='c')
%timeit pd.read_csv(filename, engine='pyarrow')
1min 49s ± 2.42 s per loop (mean ± std. dev. of 7 runs, 1 loop each)
27.6 s ± 707 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
5.35 s ± 667 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

メモリ消費量(incrementをご参照)も、pyarrowが最も省メモリでした。pythonエンジンの約1/4しか消費しないのは驚きです!

pyarrowエンジンでは併用可能な引数が少ないという制約があるものの、利用できるシーンでは積極的に使った方が良さそうです。

%memit pd.read_csv(filename, engine='python')
%memit pd.read_csv(filename, engine='c')
%memit pd.read_csv(filename, engine='pyarrow')
peak memory: 25604.86 MiB, increment: 19105.46 MiB
peak memory: 8164.46 MiB, increment: 7480.59 MiB
peak memory: 7794.48 MiB, increment: 5771.98 MiB

2. dtypeによるデータ型の指定

各カラムのデータ型をdtypeで指定した場合の効果を比較します。

実行時間では、dtypeを指定しない方が高速でした。stringdatetimeの変換処理で時間を要したものと思われます。

dtype={'int': np.uint8, 'float': np.float32, 'bool': bool, 
       'string': pd.CategoricalDtype(list(ascii_lowercase))}

%timeit pd.read_csv(filename)
%timeit pd.read_csv(filename, dtype=dtype, parse_dates=['datetime'])
26.2 s ± 816 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
35.4 s ± 928 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

メモリ消費量では、dtype指定ありの方が省メモリでした。

%memit pd.read_csv(filename)
%memit pd.read_csv(filename, dtype=dtype, parse_dates=['datetime'])
peak memory: 7680.60 MiB, increment: 6955.95 MiB
peak memory: 6393.80 MiB, increment: 5670.64 MiB

読み込み後のDataFrameのメモリサイズ(MB)をカラム毎に比較し、dtype指定によるメモリ削減率(reduce)を算出してみます。

結果は、bool型を除く全てのカラムで削減効果が得られました。特に、datetimeカラムは89%、stringカラムは98%と劇的に減っています!値のレンジや重複度合いに応じて適切なデータ型を用いることで、読み取り後のメモリサイズを大きく削減できることがあることが分かりました。

usage = pd.DataFrame()

# カラム毎のメモリサイズを取得
usage['default[MB]'] = pd.read_csv(filename).memory_usage(index=False, deep=True)
usage['dtype[MB]'] = (pd.read_csv(filename, dtype=dtype, parse_dates=['datetime'])
                      .memory_usage(index=False, deep=True))

# メモリサイズの単位をMBに変換し、削減率を算出
usage /= 10**6
usage['reduce[%]'] = (1 - usage['dtype[MB]'] / usage['default[MB]']) * 100

print(usage.round(1))
          default[MB]  dtype[MB]  reduce[%]
int             400.0       50.0       87.5
float           400.0      200.0       50.0
bool             50.0       50.0        0.0
string         2900.0       50.0       98.3
datetime       3800.0      400.0       89.5

3. convertersによる数値/文字列の変換

数値変換

サンプルデータからfloat列だけを読み込み、平方根を計算する例で比較します。

実行時間では、後者(読み込み後に数値演算)がパフォーマンスに優れていました。前者ではconvertersが各要素を1つずつ処理するのに対し、後者ではnumpyの配列演算により高速化が行われた、と推測されます。メモリ消費量では大きな差異は見られませんでした。

converters={'float': lambda f: np.sqrt(float(f))}

%timeit pd.read_csv(filename, usecols=['float'], converters=converters)
%timeit np.sqrt(pd.read_csv(filename, usecols=['float']))

%memit pd.read_csv(filename, usecols=['float'], converters=converters)
%memit np.sqrt(pd.read_csv(filename, usecols=['float']))
56.6 s ± 76.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
10.2 s ± 625 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
peak memory: 928.48 MiB, increment: 450.10 MiB
peak memory: 973.07 MiB, increment: 489.69 MiB

文字列変換

サンプルデータからstring列だけを読み込み、3桁0埋め(a00a)する例で比較します。

実行時間は意外にもconvertersを使用した方が2割ほど速く、メモリ消費量はどちらも同程度という結果となりました。簡単な文字列処理であれば、convertersを使用しても大きなボトルネックにはならなさそうです。メモリ消費量はどちらも同程度でした。

converters={'string': lambda s: s.zfill(3)}

%timeit pd.read_csv(filename, usecols=['string'], converters=converters) 
%timeit pd.read_csv(filename, usecols=['string'])['string'].str.zfill(3)

%memit pd.read_csv(filename, usecols=['string'], converters=converters) 
%memit pd.read_csv(filename, usecols=['string'])['string'].str.zfill(3)
15.1 s ± 583 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
18.9 s ± 838 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
peak memory: 4526.77 MiB, increment: 3794.86 MiB
peak memory: 4546.05 MiB, increment: 3606.62 MiB

4. parse_datesto_datetimeによる日付・時刻変換

サンプルデータからdatetime列だけを読み込み、parse_datesによる変換と、読み込み後にpandas.to_datetimeで変換するパターンで比較します。

結果は、実行時間・メモリ消費量とも近しい結果となりました。どちらを使っても良さそうですが、to_datetimeは変換エラー時の挙動やタイムゾーン⇒UTCへの変換といった便利機能が多数実装されているため、より高度な変換要件がある場合はto_datetimeが便利です。

%timeit pd.read_csv(filename, usecols=['datetime'], parse_dates=['datetime'])
%timeit pd.to_datetime(pd.read_csv(filename, usecols=['datetime'])['datetime'])

%memit pd.read_csv(filename, usecols=['datetime'], parse_dates=['datetime'])
%memit pd.to_datetime(pd.read_csv(filename, usecols=['datetime'])['datetime'])
31.6 s ± 870 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
31.7 s ± 1.13 s per loop (mean ± std. dev. of 7 runs, 1 loop each)
peak memory: 6057.39 MiB, increment: 5322.44 MiB
peak memory: 5676.00 MiB, increment: 4941.04 MiB

5. cache_datesによるキャッシュ効果

cache_datesによるキャッシュ効果を、3つの日付フォーマットで比較検証します。

事前に、重複した日付を含む5,000万件のDataFrameを生成し、各日付フォーマットでCSV出力します。

# 5万件のユニークな日付を1000回複製
dates = pd.Series(pd.date_range('2023/4/1', periods=50000).repeat(1000), name='Date')

# ISO、dayfirst、日本語の3フォーマットでそれぞれ保存
dates.to_csv('dates_isoformat.csv', index=False)
dates.dt.strftime('%d/%m/%Y').to_csv('dates_dayfirst.csv', index=False)
dates.dt.strftime('%Y年%m月%d日').to_csv('dates_japan.csv', index=False)

del dates

ISO8601フォーマットでは、実行時間が3割ほど短縮されました。pandasのユーザーガイドにもparse_datesはISO8601に最適化されているとの記載があるため、このフォーマットであればキャッシュが無くても十分に高速なようです。

filename = 'dates_isoformat.csv'

%timeit pd.read_csv(filename, parse_dates=['Date'], cache_dates=False)
%timeit pd.read_csv(filename, parse_dates=['Date'])

%memit pd.read_csv(filename, parse_dates=['Date'], cache_dates=False)
%memit pd.read_csv(filename, parse_dates=['Date'])
11.4 s ± 883 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
8.06 s ± 564 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
peak memory: 2270.68 MiB, increment: 1525.89 MiB
peak memory: 1986.41 MiB, increment: 1241.62 MiB

dayfirstフォーマットでは、実行時間が9割近くも大幅削減されました!

filename = 'dates_dayfirst.csv'

%timeit pd.read_csv(filename, parse_dates=['Date'], dayfirst=True, cache_dates=False)
%timeit pd.read_csv(filename, parse_dates=['Date'], dayfirst=True)

%memit pd.read_csv(filename, parse_dates=['Date'], dayfirst=True, cache_dates=False)
%memit pd.read_csv(filename, parse_dates=['Date'], dayfirst=True)
1min 29s ± 870 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
8.18 s ± 564 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
peak memory: 2269.60 MiB, increment: 1525.80 MiB
peak memory: 1989.43 MiB, increment: 1245.62 MiB

日本語のフォーマットでも、約8割ほど実行時間が削減されました。

filename = 'dates_japan.csv'

%timeit pd.read_csv(filename, parse_dates=['Date'], date_format='%Y年%m月%d日', \
                    cache_dates=False)
%timeit pd.read_csv(filename, parse_dates=['Date'], date_format='%Y年%m月%d日')

%memit pd.read_csv(filename, parse_dates=['Date'], date_format='%Y年%m月%d日', \
                   cache_dates=False)
%memit pd.read_csv(filename, parse_dates=['Date'], date_format='%Y年%m月%d日')
1min 31s ± 1.1 s per loop (mean ± std. dev. of 7 runs, 1 loop each)
11.2 s ± 660 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
peak memory: 2216.78 MiB, increment: 1525.82 MiB
peak memory: 1835.72 MiB, increment: 1144.76 MiB

以上の結果から、どの日付フォーマットでもキャッシュは有効であり、変換処理に時間が掛かるほどキャッシュ効果が大きいことが確認できました。

また、キャッシュしない方がなぜかメモリ消費量が増えることも分かりました(原因は不明)。キャッシュはデフォルトで有効化されているため、あえて無効化する必要は無さそうです。

その他テクニック

最後となる本章では、「各引数をベタ書きせず、コレクションとして定義するテクニック」をご紹介します。read_csvと各引数を別々に実装・管理することで、

  • 引数を別のモジュールやファイルで管理し、プログラムを修正せずに引数の値だけを変更できるようにしたい
  • スキーマが同じ(もしくは近しい)複数のCSVファイルを読み込む際に、引数を共通利用したい

といったニーズに応えることができます。本章では、dictdataclassyamltomlの4種類で引数を定義し、それぞれの特徴やメリットを確認していきます。

説明にあたり、国土交通省が公開している日本標準時基準の台風発生数のCSVデータを使用します。先頭5行と末尾5行のファイル内容は以下となります。

$ wget -q -O typhoon.csv https://www.data.jma.go.jp/yoho/typhoon/statistics/generation/generation_j.csv
$ (head -5;tail -5) < typhoon.csv | iconv -f SJIS
年,1月,2月,3月,4月,5月,6月,7月,8月,9月,10月,11月,12月,年間
1951,,1,1,2,1,1,3,3,2,4,1,2,21
1952,,,,,,3,3,5,3,6,3,4,27
1953,,1,,,1,2,1,6,3,5,3,1,23
1954,,,1,,1,,1,5,5,4,3,1,21
2018,1,1,1,,,4,5,9,4,1,3,,29
2019,1,1,,,,1,4,5,6,4,6,1,29
2020,,,,,1,1,,8,3,6,3,1,23
2021,,1,,1,1,2,3,4,4,4,1,1,22
2022,,,,2,,1,3,5,7,5,1,1,25

今回は以下要件でCSVを読み込むことにします。

  • 文字コードはSHIFT-JIS形式とする
  • 1月12月の計13カラムのみを読み込む(年間カラムはスキップ)
  • カラムをインデックスに指定する
  • ヘッダー行を除いた先頭5行のみを読み込む
  • 各カラムのデータ型は、欠損値を許容する整数型とする

要件の実装例は以下となります。合計6つの引数を直接指定しましたが、以降では各引数を様々な方法で定義してみます。

df = pd.read_csv('typhoon.csv', 
                 encoding='shift-jis', 
                 usecols='年 1月 2月 3月 4月 5月 6月 7月 8月 9月 10月 11月 12月'.split(),
                 index_col='', 
                 nrows=5,
                 dtype=pd.UInt16Dtype())
print(df)
       1月   2月   3月   4月   5月   6月  7月  8月  9月  10月  11月  12月
年                                                                       
1951  <NA>     1     1     2     1     1    3    3    2     4     1     2
1952  <NA>  <NA>  <NA>  <NA>  <NA>     3    3    5    3     6     3     4
1953  <NA>     1  <NA>  <NA>     1     2    1    6    3     5     3     1
1954  <NA>  <NA>     1  <NA>     1  <NA>    1    5    5     4     3     1
1955     1     1     1     1  <NA>     2    7    6    4     3     1     1

引数をdictで定義

最もシンプルなパターンです。dictでは位置引数は指定できないため、ファイル名もfilepath_or_bufferというキー名で指定します。

あとはread_csv(**params)とするだけで、全ての引数が展開されます。引数の定義と読み込み処理を分離することで、コードの見通しがとても良くなりました。

dictを使うメリットは2点です。

  • 組み込み関数のため、ライブラリの追加インストールが不要
  • ミュータブル/イミュータブルを問わず、任意のオブジェクトを定義できる
params = dict(filepath_or_buffer='typhoon.csv', 
              encoding='shift-jis', 
              usecols='年 1月 2月 3月 4月 5月 6月 7月 8月 9月 10月 11月 12月'.split(),
              index_col='', 
              nrows=5, 
              dtype=pd.UInt16Dtype())

df = pd.read_csv(**params)
print(df)
       1月   2月   3月   4月   5月   6月  7月  8月  9月  10月  11月  12月
年                                                                       
1951  <NA>     1     1     2     1     1    3    3    2     4     1     2
1952  <NA>  <NA>  <NA>  <NA>  <NA>     3    3    5    3     6     3     4
1953  <NA>     1  <NA>  <NA>     1     2    1    6    3     5     3     1
1954  <NA>  <NA>     1  <NA>     1  <NA>    1    5    5     4     3     1
1955     1     1     1     1  <NA>     2    7    6    4     3     1     1

引数をdataclassesで定義

dataclassesは、Python3.7から実装されたデータ定義用の標準ライブラリです。引数用のクラスを定義し、asdictで辞書に変換することで引数を展開します。

ここでは、usecolsの値をリストではなくタプルで定義しています。fieldを使えばリストも定義可能ですが、usecolsにはタプルも指定できることと、dataclassesのフィールドはイミュータブルの方がが望ましいとの考えから、今回はタプルとしました。

dataclassesを使用するメリットは2点です。

  • 外部パッケージが不要
  • 引数のアノテーションや型チェックができる
from dataclasses import dataclass, asdict

@dataclass
class Params:
    filepath_or_buffer: str = 'typhoon.csv'
    encoding: str = 'shift-jis'
    usecols: tuple = ('', '1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月')
    index_col: str = ''
    nrows: int = 5
    dtype: pd.core.arrays.integer.IntegerDtype = pd.UInt16Dtype()
    
df = pd.read_csv(**asdict(Params()))
print(df)
       1月   2月   3月   4月   5月   6月  7月  8月  9月  10月  11月  12月
年                                                                       
1951  <NA>     1     1     2     1     1    3    3    2     4     1     2
1952  <NA>  <NA>  <NA>  <NA>  <NA>     3    3    5    3     6     3     4
1953  <NA>     1  <NA>  <NA>     1     2    1    6    3     5     3     1
1954  <NA>  <NA>     1  <NA>     1  <NA>    1    5    5     4     3     1
1955     1     1     1     1  <NA>     2    7    6    4     3     1     1

引数をyamlで定義

メジャーな設定ファイルフォーマットの1つであるyamlで定義するパターンです。pythonでは、pyyaml等の外部パッケージを使用して読み書きします。

ポイントはdtypeの指定方法です。dictdataclassesのようにpd.UInt16Dtypes()といったpythonオブジェクトを直接定義できないため、同じデータ型を意味するUInt16という文字列に置き換えて定義します。

yamlで定義するメリットは2点です。

  • プログラミング言語に依存しない、メジャーな設定ファイルフォーマットで定義できる
  • 引数をファイルで管理できる
$ cat params.yaml
filepath_or_buffer: typhoon.csv
encoding: shift-jis
index_col: 年
nrows: 5
usecols: 
  - 年
  - 1月
  - 2月
  - 3月
  - 4月
  - 5月
  - 6月
  - 7月
  - 8月
  - 9月
  - 10月
  - 11月
  - 12月
dtype: UInt16
import yaml

with open('params.yaml') as f:
    params = yaml.safe_load(f)
    
df = pd.read_csv(**params)
print(df)
       1月   2月   3月   4月   5月   6月  7月  8月  9月  10月  11月  12月
年                                                                       
1951  <NA>     1     1     2     1     1    3    3    2     4     1     2
1952  <NA>  <NA>  <NA>  <NA>  <NA>     3    3    5    3     6     3     4
1953  <NA>     1  <NA>  <NA>     1     2    1    6    3     5     3     1
1954  <NA>  <NA>     1  <NA>     1  <NA>    1    5    5     4     3     1
1955     1     1     1     1  <NA>     2    7    6    4     3     1     1

引数をtomlで定義

tomlも注目を集めている設定ファイルフォーマットの1つです。python3.11でtomllibが標準ライブラリに追加され、tomlファイルの読み込みが出来るようになりました。

params.tomlに引数を定義して、tomllib.load()で読み込みます。バイナリモード(rb)で読み込んでいるのはtomllibの仕様によるものです。

tomlを使用するメリットは3点です。

  • 外部パッケージが不要(python 3.11以降)
  • プログラミング言語に依存しない、メジャーな設定ファイルフォーマットで定義できる
  • 引数をファイルで管理できる
$ cat params.toml
filepath_or_buffer = "typhoon.csv"
encoding = "shift-jis"
index_col = "年"
nrows = 5
usecols = ["年", "1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"]
dtype = "UInt16"
import tomllib

with open('params.toml', 'rb') as f:
    params = tomllib.load(f)
    
df = pd.read_csv(**params)
print(df)
       1月   2月   3月   4月   5月   6月  7月  8月  9月  10月  11月  12月
年                                                                       
1951  <NA>     1     1     2     1     1    3    3    2     4     1     2
1952  <NA>  <NA>  <NA>  <NA>  <NA>     3    3    5    3     6     3     4
1953  <NA>     1  <NA>  <NA>     1     2    1    6    3     5     3     1
1954  <NA>  <NA>     1  <NA>     1  <NA>    1    5    5     4     3     1
1955     1     1     1     1  <NA>     2    7    6    4     3     1     1

まとめ

本記事では、pandas.read_csv関数における大量の引数について、その効果や制約、パフォーマンス、各引数の管理方法をご紹介しました。

世の中にはCSVよりもずっと高効率なデータフォーマットが多数ありますが、既存システムとの互換性といった諸事情から、CSVは今後もしばらくは使い続けられるでしょう。本記事を通じて、そんなCSVの取り扱いが少しでも便利になれば幸いです。

長文にお付き合いいただき、ありがとうございました。

63
62
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
63
62

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?