取得したデータをどこかに保存するとき、まず が浮かびます。Excel で開けて、テキストエディタでも中身が見えて、どんな道具でも読めます。共有には確かに向いています。
問題は、 が型を持たないことです。列に何が入るかという情報が、どこにも書かれていません。保存した時点で全部が文字列になり、読み直すときに推測されます。この推測が毎回同じとは限らず、しかも間違えても例外は出ません。取得したデータの保存先としては、この性質が効いてきます。
CSV で実際に壊れるもの
壊れ方には決まった型があります。どれも「エラーにならず、値だけが変わる」ので、気づくのは集計がおかしいと言われたときです。以下は手元で実際に書いて読み直し、値と型がどう変わるかを確かめた結果です。
| 元の値と型 | 読み直した結果 | 何が起きたか |
|---|---|---|
| "0120003"(文字列) | 120003(整数) | 先頭のゼロが落ちる |
| "7203"(文字列) | 7203(整数) | 文字列だった前提が崩れる |
| 2026-09-02(日付型) | "2026-09-02"(文字列) | 日付として扱えない |
| "" と欠損の混在 | どちらも欠損 | 空文字と欠損の区別が消える |
| "NA"(国名などの文字列) | 欠損 | 値が黙って消える |
| 0.30000000000000004 | 0.3 | 小数の精度が落ちる |
上の表は実際に書いて読み直して確かめたものです。真偽値・大きな整数・カンマや改行を含む文字列は正しく復元されました。「CSV は何もかも壊す」わけではありません。ただし復元されるかどうかは読み込み側の推測に依存するので、の版が変われば結果も変わりえます。
先頭ゼロが落ちる問題は特に厄介です。郵便番号、証券コード、電話番号、商品コード。識別子として使う値ほど先頭ゼロを持ちやすく、落ちると突き合わせに失敗します。しかも一部の行だけ失敗するので、件数が少し減るだけで済んでしまいます。
読み込むときに列の型を明示すれば防げますが、それは列の一覧と型を常に把握し続けるということです。列が増えるたびに指定を足す必要があり、忘れれば元に戻ります。人の注意力に頼る対策は、いずれ破れます。
欠損の扱いも見落としやすい点です。「値が無い」のか「空文字が入っている」のかは、意味がまったく違います。前者は取得できなかった、後者は取得したうえで空だった、という違いです。 に書くと両方とも空欄になり、読み直したときに区別が付きません。実測でも、空文字と欠損を並べて書いたものは、両方とも欠損として返ってきました。
Parquet なら型が残る
は列ごとに型と値をまとめて持つ形式です。書いたときの型がそのまま残るので、読み直しても推測は起きません。文字列は文字列、日付は日付、欠損は欠損として区別されます。
副次的な利点として、同じデータでもファイルが小さくなります。列ごとに似た値が並ぶので圧縮が効くためです。さらに必要な列だけを読めるので、100列あるうち3列しか使わないなら、その3列だけを読み込めます。データが増えるほど、この差は効いてきます。
型が残ることには、もう一つ効果があります。に気づけるようになることです。配信元が数値だった列を文字列に変えた場合、 では気づけませんが、Parquet では型が違うファイルとして残るので、読み込み時に食い違いが表に出ます。
書き出したものの中身を確かめたいときは、を使うと Parquet に直接問い合わせを書けます。読み込んでから調べるのではなく、ファイルに対してそのまま集計や件数確認ができるので、検算の手数が減ります。pandasに読み込むほどでもない確認に向いています。
import pandas as pd
df = pd.DataFrame( { "zip": ["0120003", "1500001"], # 先頭ゼロを持つ文字列 "code": ["7203", "6758"], "date": pd.to_datetime(["2026-09-02", "2026-09-03"]), "flag": [True, False], "value": [1234.5, None], })
df.to_csv("out.csv", index=False)df.to_parquet("out.parquet", index=False)
back_csv = pd.read_csv("out.csv")back_pq = pd.read_parquet("out.parquet")
print("CSV zip:", back_csv["zip"].tolist(), back_csv["zip"].dtype)print("Parquet zip:", back_pq["zip"].tolist(), back_pq["zip"].dtype)print("CSV date dtype:", back_csv["date"].dtype)print("Parquet date dtype:", back_pq["date"].dtype)人に渡すときは が最適です。相手の道具を選ばないという利点は大きい。ここで言っているのは、処理の途中で保存する形式の話です。人に渡す直前に CSV へ書き出せば、両方の利点を取れます。
日付で分けて置く
Parquet は1ファイルに全部入れるのではなく、日付などで分けて置くと扱いやすくなります。これがです。必要な期間のファイルだけを読めばよくなるので、データが増えても読み込み時間が伸びにくくなります。
from pathlib import Path
import pandas as pd
def save_partitioned(df: pd.DataFrame, base: Path, date_col: str = "fetched_date"): """取得日ごとにファイルを分けて書く。同じ日を2回書いても結果は同じ。""" for day, part in df.groupby(date_col): d = base / f"dt={day}" d.mkdir(parents=True, exist_ok=True) part.drop(columns=[date_col]).to_parquet(d / "part.parquet", index=False)
def load_range(base: Path, start: str, end: str) -> pd.DataFrame: paths = [p for p in sorted(base.glob("dt=*/part.parquet")) if start <= p.parent.name[3:] <= end] if not paths: return pd.DataFrame() return pd.concat([pd.read_parquet(p) for p in paths], ignore_index=True)ファイルを分ける単位は、あとで読む単位に合わせてください。日単位で読むなら日で分け、月単位でしか見ないなら月で分ける。細かく分けすぎると小さなファイルが大量にでき、読み込みの回数が増えて逆に遅くなります。この形にしておくと、EP.07 で扱った再開可能な取得とも噛み合います。日ごとに書き切るので、途中で落ちてもその日のぶんだけやり直せば済みます。次回は、保存したものをその場で検算する道具を扱います。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。