データの読み書き

はじめに

Python でデータを読み書きするときは pandas(パンダズ)というライブラリを使うのが基本です。pandas の名前は panel data が由来だそうですが、パンダ 🐼 とかけているのでしょう。

CSV ファイルの読み込み

ファイル名または URL を指定して CSV ファイルを読み込みます:

import pandas as pd

df = pd.read_csv("filename.csv")
df   # 長い場合は最初と最後だけ表示してくれる

具体的に気象庁の「世界の年平均気温偏差」データを読み込んでみましょう。デフォルトの文字コードは UTF-8 ですが、これは Shift JIS ですので、オプションが必要です:

import pandas as pd

df = pd.read_csv("https://www.data.jma.go.jp/cpdinfo/temp/list/csv/an_wld.csv",
                 encoding="sjis")
df

Google Colaboratory なら対話型テーブルにして眺めることもできます:

interactive table

読み込んだデータは DataFrame オブジェクトです:

type(df)
pandas.core.frame.DataFrame

最大表示行数・列数を調べたり変えたりしたいときは pd.options.display.max_rowspd.options.display.max_columns を見たり代入したりします。

表示精度を変えたいときは pd.options.display.precision(デフォルト6)に桁数の目標値を代入します(IPython の %precision には影響を受けないようです)。

read_csv() でよく使うオプション:

デフォルトでは次の文字列が欠測値(NaN)と判断されます:

'', '#N/A', '#N/A N/A', '#NA', '-1.#IND', '-1.#QNAN', '-NaN', '-nan',
'1.#IND', '1.#QNAN', '', 'N/A', 'NA', 'NULL', 'NaN', 'n/a', 'nan', 'null'

空文字列と #N/A はExcelでも欠測値と解釈されますが、# をコメント開始文字と設定している場合(comment="#")には "#N/A" のようにダブルクォートで囲む必要があります。

これ以外の欠測値用文字列が必要なときは na_valueskeep_default_na(デフォルトは True)で指定できます。空欄だけを欠測値としたい場合は次のようにします:

df = pd.read_csv("finename.csv", na_values="", keep_default_na=False)

以下の記述について、いま確認したら、結果は 0.0 つまり誤差は出ませんでした。改善されたのでしょう。念のため残しておきます:

10進で切りの良い値なのに微妙な誤差が入ることがあります。例:

from io import StringIO

data = ("A\n1258.477\n")
df = pd.read_csv(StringIO(data))
df.A[0] - 1258.477    # -2.2737367544323206e-13

これを避けるには float_precision="high" または float_precision="round_trip" というオプションを与えます。

Excel ファイルの読み込み

pandas の read_excel() を使うのが簡単ですが、xlrd パッケージが xlsx 非対応になったため、注意が必要です。あらかじめ xlrd と openpyxl パッケージを pip install xlrd openpyxl などとしてインストールしておきます。

df = pd.read_excel("filename.xlsx") # xlsも読める

pandas によるファイル読み込みの全般については pandas の IO Tools のページをご覧ください。

xlsx の読み込みは、ほかに pylightxl という軽量なライブラリがあります。

DataFrame の操作

pandas の DataFrame は R のデータフレームとほぼ同じです。簡単な解説が 10 Minutes to pandas にあります。pandasによるクエリも参考にしてください。

df          # 全体を表示
df.head()   # 頭の5行だけ表示
df.tail()   # 末尾の5行だけ表示
df.shape    # 行数・列数
df.columns  # 各列の名前
df.dtypes   # 各列の名前とデータ型を表示
df.index    # 行の名前(デフォルトでは 0 から始まる行番号)
df.describe() # 記述統計量を表示
df.to_latex() # LaTeXのtabular形式に変換
df1 = df.dropna() # 欠測値のある行を外す
df1 = df.fillna(0) # 欠測値を0で置き換える
df1 = df.fillna(method="ffill") # 欠測値を直前の行で置き換える
df["A"] = df["A"].apply(float)  # 列の各要素に関数(ここでは float())を適用

データの(0から数えて)例えば2行目から4行目を選択するには df[2:5] のようにします。データの一部の列を選択するには df.列名 あるいは df['列名'] あるいは df[['列名1','列名2']] のようにします。行列のように行名・列名で選ぶには df.loc['行名','列名']、行番号・列番号で選ぶには df.iloc[行番号,列番号] とします。複数与えるには [2,3,4] のようにリストにするか、2:5 のように範囲で表します(Pythonでは番号は0から数え、2:5 のような範囲は最後の5を含まないことに注意)。

df['列名'] はSeries(1列だけのDataFrame)という型のものです。普通のベクトル(ndarray)に直すには df['列名'].values とします。

df['列名'].value_counts(sort=False)  # 列の度数分布表
df['列名'].mean()  # 列の平均(median(), var(), std() なども同様)
df.groupby('列名1')['列名2'].mean() # 列名1でグループ化した列名2の平均
pd.crosstab(df['列名1'], df['列名2']) # クロス集計

場合によっては df.groupby("...").sum() のようなコードは df.groupby("...").sum(numeric_only=True) にしないと警告が出るようになりました。

データ型の指定

この節については、いしおさんのブログDataFrameのメモリサイズを節約するが参考になります。

例えば shinjuku.csv というCSVファイルを読み込むとしましょう。

import matplotlib.pyplot as plt
import pandas as pd
import sys

url = "https://okumuralab.org/~okumura/stat/data/shinjuku.csv"
df1 = pd.read_csv(url)
df1.shape           # (25705, 4)
sys.getsizeof(df1)  # 2493489

df1.head() の結果は次のようになります:

           datetime     max     min     avg
0  2011/03/01 00:00  0.0370  0.0316  0.0338
1  2011/03/01 01:00  0.0368  0.0324  0.0342
2  2011/03/01 02:00  0.0357  0.0325  0.0340
3  2011/03/01 03:00  0.0371  0.0322  0.0343
4  2011/03/01 04:00  0.0363  0.0321  0.0342

まず、日付はインデックスに移動してみましょう:

df2 = pd.read_csv(url, index_col='datetime')
df2.shape           # (25705, 3)
sys.getsizeof(df2)  # 2493409

サイズの節約にはなりませんでした。df2.head() は次のようになります:

                     max     min     avg
datetime                                
2011/03/01 00:00  0.0370  0.0316  0.0338
2011/03/01 01:00  0.0368  0.0324  0.0342
2011/03/01 02:00  0.0357  0.0325  0.0340
2011/03/01 03:00  0.0371  0.0322  0.0343
2011/03/01 04:00  0.0363  0.0321  0.0342

日付が文字列で読み込まれてしまうのがサイズを大きくする原因です。日時データは parse_dates に指定して、pandasの64ビットの「Timestamp」型にします。この型は1970年元旦を起点としたナノ秒単位の整数で、1677年から2262年まで使えます(pd.Timestamp.minpd.Timestamp.max)。残りの列は特に何もする必要はないのですが、ここではメモリが逼迫していると仮定して、32ビットの浮動小数点型にしてみます:

df3 = pd.read_csv(url, index_col='datetime', parse_dates=['datetime'],
                  dtype={'max':'float32', 'min':'float32', 'avg':'float32'})
sys.getsizeof(df3)  # 514124

列指定は番号でもかまいません:

df4 = pd.read_csv(url, index_col=0, parse_dates=[0],
                  dtype={1:'float32', 2:'float32', 3:'float32'})
sys.getsizeof(df4)  # 514124

インデックス以外の列指定が同じなら、次のようにしてもかまいません:

df5 = pd.read_csv(url, index_col=0, parse_dates=[0], dtype='float32')
sys.getsizeof(df5)  # 514124

データ型としては、符号付き整数 'int8''int16''int32''int64'、符号なし整数 'uint8''uint16''uint32''uint64'、浮動小数点型 'float16''float32''float64''float128'、論理型 'bool'True または False)などが使えるはずです。次のような簡単なプログラムで確認できます:

from io import StringIO
data = ("A,B\n1,1.5\n2,2.5")
df = pd.read_csv(StringIO(data), dtype={0:'int8', 1:'float16'})
sys.getsizeof(df)
df.head()

データ型を指定しないと自動判断しようとしますが、うまくいかないと「Columns ... have mixed types」のような警告を出します。low_memory=False オプションで、より丁寧に自動判断してくれます。

真偽値の文字列は TrueFalse がデフォルトですが、true_values=['Yes'], false_values=['No'] のようなオプションで指定できます。

「Timestamp」型の欠測値(空文字列、NA など)は NaN でなく NaT(Not-a-Time)になります(pd.NaT)。

日時のデータ型については日時もご参照ください。

データの書き出し

データフレームのCSV形式での書き出しは pandas.DataFrame.to_csv() を使います。デフォルトはUTF-8ですが、Excel互換のBOM付きUTF-8(行末CRLF)でインデックスを付けずに保存するには次のようにします:

df.to_csv("filename.csv", index=False, encoding="utf_8_sig", lineterminator="\r\n")

データが10進で切りの良い値でも、微妙な誤差が入ることがあります。例えばオプション float_format="%.14g" を付けて、わざと精度を落として書き込むといった対策がありそうです。

Excel形式での書き出しは pandas.DataFrame.to_excel() です。エンジンは openpyxl または xlsxwriter が指定できます。前者のほうが多機能のようです。あらかじめ pip install openpyxl などと打ち込んでインストールしておきます。

df.to_excel("filename.xlsx")

JSON形式への変換・出力は to_json() です。lines=True で改行区切りJSON(JSONL = JSON Lines、NDJSON = Newline Delimited JSON、LDJSON = Line Delimited JSON)になります:

df = pd.DataFrame({"A":[1,2],"B":[1.5,2.5]})
df.to_json()  #==> '{"A":{"0":1,"1":2},"B":{"0":1.5,"1":2.5}}'
df.to_json(orient="records")
#==> '[{"A":1,"B":1.5},{"A":2,"B":2.5}]'
df.to_json(orient="records", lines=True)
#==> '{"A":1,"B":1.5}\n{"A":2,"B":2.5}'
df.to_json('hoge.jsonl', orient="records", lines=True) # ファイルに出力

JSONL形式の読み込み:

df = pd.read_json('hoge.jsonl', orient='records', lines=True)

ちなみに、JSONファイルの操作には jq が便利です(MacならHomebrewで入れられます)。

DataFrame を dict 形式にするには:

import json

json.dumps(df.to_dict(orient="list"))
#==> '{"A": [1, 2], "B": [1.5, 2.5]}'
json.dumps(df.to_dict(orient="list"), separators=(',', ':'))
#==> '{"A":[1,2],"B":[1.5,2.5]}'

いったん dict 形式になれば、JSON ファイルへの書き出し・読み込みは次のようにできます:

with open('hoge.json', 'w', encoding='utf-8') as f:
    json.dump(hoge, f)

with open('hoge.json', encoding='utf-8') as f:
    hoge = json.load(f)

この open() はPython標準の関数で、文字コード encoding はシステム依存です(Python 3.11以降では locale.getencoding() で調べられます。Windows日本語環境では 'cp932'、それ以外ではだいたい 'UTF-8' です)。

全部メモリに入れないで処理する

全体を DataFrame に読み込まず行単位で操作するには、標準ライブラリ csvcsv.reader()csv.writer() を使います。例:

import csv

with open('file.csv', encoding='utf-8') as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)

別の方法として、次のように少しずつ読み込む方法があります。次の例では1000行ずつ読み込んでいます。この場合 df は DataFrame を読み込むためのイテレータになります。

df = pd.read_csv('file.csv', chunksize=1000)

for chunk in df:
    print(chunk.head())

並列処理

Modin を使えば、import pandas as pdimport modin.pandas as pd と書き換えるだけで pandas が高速になります。

キャッシュ

他サイトから何度も読むデータについては、キャッシュすることが推奨されます。カレントディレクトリに .web_cache というディレクトリができ、その中にキャッシュが保存されます。サイトへのアクセスは毎回しますが、前回から変わってない(ステータスコードが200ではなく304)場合はキャッシュから読みます。

import pandas as pd
from io import StringIO
import requests
from cachecontrol import CacheControl
from cachecontrol.caches.file_cache import FileCache

URL = "https://example.org/test.csv"

cache = FileCache('.web_cache', forever=True)
sess = CacheControl(requests.Session(), cache)
r = sess.get(URL)
r.encoding = "UTF-8"  # または "Shift_JIS"
df = pd.read_csv(StringIO(r.text))