ふくふくHukuhuku Inc.
EP.12Data Fetch 9分公開: 2026-09-02

CSV をやめて Parquet にする ── 型が消えない保存形式

CSV は誰でも開けますが、型を持ちません。保存して読み直すたびに、日付が文字列になり、先頭ゼロが消えます。取得したデータの保存先としては向きません。

#Python#Parquet#データ基盤
執筆 / 監修
松尾 亮合同会社ふくふく 代表社員

データ基盤・データパイプライン構築 / BI / 生成 AI 活用支援を専門とするエンジニア (28 年)。 本記事は AI 利用ポリシーに基づき、生成 AI の補助で執筆 → 人間が監修・編集して公開しています。

プロフィール詳細
シェア

取得したデータをどこかに保存するとき、まず が浮かびます。Excel で開けて、テキストエディタでも中身が見えて、どんな道具でも読めます。共有には確かに向いています。

問題は、 が型を持たないことです。列に何が入るかという情報が、どこにも書かれていません。保存した時点で全部が文字列になり、読み直すときに推測されます。この推測が毎回同じとは限らず、しかも間違えても例外は出ません。取得したデータの保存先としては、この性質が効いてきます。

CSV で実際に壊れるもの

壊れ方には決まった型があります。どれも「エラーにならず、値だけが変わる」ので、気づくのは集計がおかしいと言われたときです。以下は手元で実際に書いて読み直し、値と型がどう変わるかを確かめた結果です。

CSV に書いて読み直したときの実測(pandas 3.0.2)
元の値と型読み直した結果何が起きたか
"0120003"(文字列)120003(整数)先頭のゼロが落ちる
"7203"(文字列)7203(整数)文字列だった前提が崩れる
2026-09-02(日付型)"2026-09-02"(文字列)日付として扱えない
"" と欠損の混在どちらも欠損空文字と欠損の区別が消える
"NA"(国名などの文字列)欠損値が黙って消える
0.300000000000000040.3小数の精度が落ちる
真偽値と大きな整数は保たれた

上の表は実際に書いて読み直して確かめたものです。真偽値・大きな整数・カンマや改行を含む文字列は正しく復元されました。「CSV は何もかも壊す」わけではありません。ただし復元されるかどうかは読み込み側の推測に依存するので、の版が変われば結果も変わりえます

先頭ゼロが落ちる問題は特に厄介です。郵便番号、証券コード、電話番号、商品コード。識別子として使う値ほど先頭ゼロを持ちやすく、落ちると突き合わせに失敗します。しかも一部の行だけ失敗するので、件数が少し減るだけで済んでしまいます。

読み込むときに列の型を明示すれば防げますが、それは列の一覧と型を常に把握し続けるということです。列が増えるたびに指定を足す必要があり、忘れれば元に戻ります。人の注意力に頼る対策は、いずれ破れます。

欠損の扱いも見落としやすい点です。「値が無い」のか「空文字が入っている」のかは、意味がまったく違います。前者は取得できなかった、後者は取得したうえで空だった、という違いです。 に書くと両方とも空欄になり、読み直したときに区別が付きません。実測でも、空文字と欠損を並べて書いたものは、両方とも欠損として返ってきました。

Parquet なら型が残る

は列ごとに型と値をまとめて持つ形式です。書いたときの型がそのまま残るので、読み直しても推測は起きません。文字列は文字列、日付は日付、欠損は欠損として区別されます。

副次的な利点として、同じデータでもファイルが小さくなります。列ごとに似た値が並ぶので圧縮が効くためです。さらに必要な列だけを読めるので、100列あるうち3列しか使わないなら、その3列だけを読み込めます。データが増えるほど、この差は効いてきます。

型が残ることには、もう一つ効果があります。に気づけるようになることです。配信元が数値だった列を文字列に変えた場合、 では気づけませんが、Parquet では型が違うファイルとして残るので、読み込み時に食い違いが表に出ます。

書き出したものの中身を確かめたいときは、を使うと Parquet に直接問い合わせを書けます。読み込んでから調べるのではなく、ファイルに対してそのまま集計や件数確認ができるので、検算の手数が減ります。pandasに読み込むほどでもない確認に向いています。

型が往復で保たれることを、実際に確かめる
Python
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 を使うなという話ではない

人に渡すときは が最適です。相手の道具を選ばないという利点は大きい。ここで言っているのは、処理の途中で保存する形式の話です。人に渡す直前に CSV へ書き出せば、両方の利点を取れます。

日付で分けて置く

Parquet は1ファイルに全部入れるのではなく、日付などで分けて置くと扱いやすくなります。これがです。必要な期間のファイルだけを読めばよくなるので、データが増えても読み込み時間が伸びにくくなります。

取得日で分けて保存する。読むときは範囲を指定できる
Python
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 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。

シリーズの外も探す:

まずは、現状を聞かせてください。

要件が固まっていなくて大丈夫です。現状診断と方針提案までを無料でお手伝いします。

無料相談フォームへ hello [at] hukuhuku [dot] co [dot] jp