pandasで前処理や集計を終えたあと、「DataFrameをCSVに保存したいのに、ファイルがどこにあるか分からない」「開いたら左端に余計な列が増えた」「日本語が文字化けした」と困ることがあります。
DataFrameをCSVへ保存する基本形は、次の1行です。
df.to_csv("sales.csv", index=False)
ただし、正しく再利用できるCSVにするには、コードを書く前に次の順で決めることが大切です。
- どこへ保存するか
- インデックスを残すか
- どのアプリで開くかに応じて文字コードを選ぶ
- どの列を保存するか
- 同名ファイルを上書きしてよいか
この記事では、Google Colabで再現できる売上データを使い、保存されたCSVをもう一度読み込んで、行数・列名・日本語が正しいところまで確認します。
この記事でわかること
to_csv()でDataFrameをCSVへ保存する基本形- 保存先、
index、encoding、columnsの選び方 headerとsepで出力形式を変える方法- 余計な
Unnamed: 0、文字化け、保存先不明、上書きの確認方法 read_csv()で読み込み直し、保存結果を検算する方法
to_csv()は、前処理・抽出・集計した結果を、共有や再利用のためにファイルへ書き出す処理です。データを整える前工程は、Pandasデータ前処理の基本で確認できます。
先に結論:保存先→index→文字コード→列→上書きの順に決める
| 確認すること | 選ぶ基準 | 主な指定 | 注意点 |
|---|---|---|---|
| 保存先 | Colab終了後も残すか | 一時領域の相対パス/Google Driveの絶対パス | Colabの一時領域はランタイム終了後に消える |
| インデックス | 行ラベルに意味があるか | 通常はindex=False |
顧客IDなど意味のある値まで消さない |
| 文字コード | どのアプリで開くか | UTF-8/utf-8-sig |
受け渡し先の指定があれば従う |
| 保存列 | 全列が必要か | columns=[...] |
列名の空白・表記違いに注意する |
| 書き込み方法 | 同名ファイルを上書きしてよいか | 通常保存/mode="x"/mode="a" |
mode="a"は行の追記であり、値の更新ではない |
最初は、実在する保存先へindex=Falseで保存し、読み込み直して確認するところから始めるのが安全です。
売上DataFrameを用意する
実行目的
同じデータを記事全体で使い、設定によってCSVの列や文字コードがどう変わるか比較します。商品IDは通常の列として保持し、DataFrameのインデックスは0から始まる連番です。
import pandas as pd
from pathlib import Path
# Colabの一時領域または現在の作業フォルダ内に保存用フォルダを作る
output_dir = Path("python_data_lab_output")
output_dir.mkdir(exist_ok=True)
df = pd.DataFrame({
"商品ID": [101, 102, 103, 104],
"商品": ["ノート", "ペン", "ファイル", "付箋"],
"カテゴリ": ["文具", "文具", "収納", "文具"],
"売上": [1200, 900, 1500, 600],
"備考": ["重点商品", None, "在庫確認", "新商品"],
})
print(df)
print("行数・列数:", df.shape)
| 商品ID | 商品 | カテゴリ | 売上 | 備考 | |
|---|---|---|---|---|---|
| 0 | 101 | ノート | 文具 | 1200 | 重点商品 |
| 1 | 102 | ペン | 文具 | 900 | None |
| 2 | 103 | ファイル | 収納 | 1500 | 在庫確認 |
| 3 | 104 | 付箋 | 文具 | 600 | 新商品 |
行数・列数: (4, 5)
出力では4行5列なので、shapeは(4, 5)です。備考の2行目にあるNoneは欠損値ですが、今回は意味のある未記入として残します。保存のためだけに0や空文字へ置き換えません。
自分のデータでは、保存前に列名・データ型・欠損値をpandas info()の使い方で確認しておくと、保存後の変化を判断しやすくなります。
基本:indexなしでCSVへ保存する
実行目的
連番インデックスをCSVへ含めず、DataFrameの5列だけを保存します。ファイル名だけを指定した相対パスは、現在の作業場所を基準に解釈されます。ここでは作成済みのpython_data_lab_outputフォルダへ保存します。
basic_path = output_dir / "sales_basic.csv"
df.to_csv(basic_path, index=False)
print("保存先:", basic_path)
print("ファイルの存在:", basic_path.exists())
print("保存フォルダ内:", sorted(p.name for p in output_dir.iterdir()))
print("CSVの先頭3行:")
print("\n".join(basic_path.read_text(encoding="utf-8").splitlines()[:3]))
保存先: python_data_lab_output/sales_basic.csv ファイルの存在: True 保存フォルダ内: ['sales_basic.csv'] CSVの先頭3行: 商品ID,商品,カテゴリ,売上,備考 101,ノート,文具,1200,重点商品 102,ペン,文具,900,
最初に見るのは、次の3点です。
ファイルの存在がTrueになっている- 1行目が
商品ID,商品,カテゴリ,売上,備考になっている - 左端に0、1のようなDataFrameの連番インデックスが追加されていない
to_csv()へ保存先を渡した場合、ファイルが作成されます。戻り値を新しいDataFrameとして使うメソッドではありません。
index=Trueとindex=Falseの違い
to_csv()では、indexの既定値がTrueです。指定を省略すると、DataFrameのインデックスもCSVへ出力されます。
| 指定 | CSVの左端 | 選ぶ場面 | 注意点 |
|---|---|---|---|
index=Trueまたは省略 |
インデックスが入る | 行ラベル自体に意味がある | 名前のない連番は再読込時に余計な列になりやすい |
index=False |
インデックスを入れない | 0始まりの連番が単なる管理番号 | 意味のあるIDをインデックスにしている場合は失う |
reset_index()後にindex=False |
意味のあるインデックスを通常列として入れる | IDを明示的な列名で残したい | 列が重複しないか確認する |
with_index_path = output_dir / "sales_with_index.csv"
without_index_path = output_dir / "sales_without_index.csv"
df.to_csv(with_index_path) # index=Trueが既定値
df.to_csv(without_index_path, index=False)
with_index = pd.read_csv(with_index_path)
without_index = pd.read_csv(without_index_path)
print("indexを保存したCSVの列:", with_index.columns.tolist())
print("index=FalseのCSVの列:", without_index.columns.tolist())
print(with_index.head(2))
indexを保存したCSVの列: ['Unnamed: 0', '商品ID', '商品', 'カテゴリ', '売上', '備考'] index=FalseのCSVの列: ['商品ID', '商品', 'カテゴリ', '売上', '備考']
| Unnamed: 0 | 商品ID | 商品 | カテゴリ | 売上 | 備考 | |
|---|---|---|---|---|---|---|
| 0 | 0 | 101 | ノート | 文具 | 1200 | 重点商品 |
| 1 | 1 | 102 | ペン | 文具 | 900 | NaN |
indexを省略したCSVを読み込むと、列一覧の先頭にUnnamed: 0が現れます。これは元のデータ列ではなく、列名のない連番インデックスが保存されたものです。
意味のあるインデックスは消さない
商品IDをインデックスにしたあとでindex=Falseにすると、CSVには商品IDが残りません。商品IDを再利用したい場合は、reset_index()で通常列へ戻してから保存します。
indexed_df = df.set_index("商品ID")
lost_id_path = output_dir / "sales_id_lost.csv"
kept_id_path = output_dir / "sales_id_kept.csv"
indexed_df.to_csv(lost_id_path, index=False)
indexed_df.reset_index().to_csv(kept_id_path, index=False)
print("index=FalseでIDを消した列:", pd.read_csv(lost_id_path).columns.tolist())
print("IDを通常列へ戻した列:", pd.read_csv(kept_id_path).columns.tolist())
index=FalseでIDを消した列: ['商品', 'カテゴリ', '売上', '備考'] IDを通常列へ戻した列: ['商品ID', '商品', 'カテゴリ', '売上', '備考']
注目するのは、前者には商品IDがなく、後者には残っている点です。index=Falseを機械的に付けるのではなく、現在のインデックスが単なる連番か、意味のある識別子かを先に確認してください。
日本語の文字化けを防ぐencodingの選び方
encodingは、CSVを利用する環境に合わせて選びます。
| 主な利用場面 | 候補 | 確認方法 | 注意点 |
|---|---|---|---|
| Pythonや多くの現代的なツールで再利用 | utf-8 |
同じencodingで読み込み直す | アプリや読み込み方法によっては日本語表示を確認する |
| 日本語をExcelで直接開いて確認 | utf-8-sig |
実際のExcel環境で開く | Excelのバージョンや開き方でも挙動が異なる |
| 受け渡し先から文字コード指定がある | 指定された文字コード | 相手先の手順で確認 | 環境を無視して固定しない |
utf-8-sigはファイル先頭にBOMを付けたUTF-8です。日本語CSVをExcelで直接開く用途では有力な候補ですが、すべての用途で一律に選ぶ必要はありません。
utf8_path = output_dir / "sales_utf8.csv"
utf8_sig_path = output_dir / "sales_utf8_sig.csv"
df.to_csv(utf8_path, index=False, encoding="utf-8")
df.to_csv(utf8_sig_path, index=False, encoding="utf-8-sig")
print("UTF-8の先頭3バイト:", utf8_path.read_bytes()[:3])
print("UTF-8 with BOMの先頭3バイト:", utf8_sig_path.read_bytes()[:3])
checked_japanese = pd.read_csv(utf8_sig_path, encoding="utf-8-sig")
print("再読み込みした商品:", checked_japanese["商品"].tolist())
UTF-8の先頭3バイト: b'\xe5\x95\x86' UTF-8 with BOMの先頭3バイト: b'\xef\xbb\xbf' 再読み込みした商品: ['ノート', 'ペン', 'ファイル', '付箋']
utf-8-sigのファイルでは、先頭3バイトがBOMを表すb'\xef\xbb\xbf'になります。再読み込み後の商品が「ノート」「ペン」「ファイル」「付箋」と表示されれば、指定した文字コードで日本語を読めています。
文字化けが起きたら、保存コードだけを見るのではなく、CSVをどのアプリで、どの文字コードとして開いたかも確認します。
必要な列だけcolumnsで保存する
実行目的
社内共有用として、商品・カテゴリ・売上だけを保存します。個人情報や作業用列を含むデータでは、保存前に共有範囲を確認することも重要です。
shared_columns = ["商品", "カテゴリ", "売上"]
shared_path = output_dir / "sales_shared.csv"
df.to_csv(
shared_path,
index=False,
encoding="utf-8-sig",
columns=shared_columns,
)
shared_df = pd.read_csv(shared_path, encoding="utf-8-sig")
print(shared_df)
print("保存した列:", shared_df.columns.tolist())
| 商品 | カテゴリ | 売上 | |
|---|---|---|---|
| 0 | ノート | 文具 | 1200 |
| 1 | ペン | 文具 | 900 |
| 2 | ファイル | 収納 | 1500 |
| 3 | 付箋 | 文具 | 600 |
保存した列: ['商品', 'カテゴリ', '売上']
出力は4行3列です。商品IDと備考が含まれず、列順もcolumnsで指定した順になっていることを確認します。
列名を間違えるとKeyErrorになります。自分のデータへ置き換える前に、df.columns.tolist()で実在する列名と前後の空白を確認してください。データ型を基準に列を選びたい場合は、pandas select_dtypes()の使い方へ進めます。
headerとsepは出力形式が必要なときだけ変更する
headerは列名を出力するか、sepは何の文字で列を区切るかを決めます。
- 通常の単独CSVでは、列名を残す既定の
header=Trueを使う - 列名が不要という明確な仕様がある場合だけ
header=Falseを使う - CSVは既定のカンマ区切り、TSVが必要な場合は
sep="\t"を使う
タブ区切りでは、文字列"t"ではなく、タブ文字を表す"\t"を指定します。
no_header_path = output_dir / "sales_no_header.csv"
tsv_path = output_dir / "sales.tsv"
df[shared_columns].to_csv(no_header_path, index=False, header=False)
df[shared_columns].to_csv(tsv_path, index=False, sep="\t")
print("header=Falseの先頭行:")
print(no_header_path.read_text(encoding="utf-8").splitlines()[0])
print("TSVの先頭行(タブを見える形に変換):")
print(tsv_path.read_text(encoding="utf-8").splitlines()[0].replace("\t", " [TAB] "))
header=Falseの先頭行: ノート,文具,1200 TSVの先頭行(タブを見える形に変換): 商品 [TAB] カテゴリ [TAB] 売上
header=Falseの先頭行は列名ではなく、最初の商品データになります。TSVの先頭行では、3つの列名がタブで区切られています。拡張子だけを.tsvへ変えても区切り文字は変わらないため、sepの指定とセットで確認します。
保存後はread_csv()で読み込み直して検算する
ファイルが存在するだけでは、必要な内容を正しく保存できたとは限りません。共有用CSVを読み込み直し、保存前後を同じ項目で比べます。
| 確認項目 | 保存前 | 再読み込み後 | 判断 |
|---|---|---|---|
| 行数 | 共有対象の4行 | 4行 | 一致するか |
| 列数 | 指定した3列 | 3列 | 余計な列や欠落がないか |
| 列名 | 商品・カテゴリ・売上 | 同じ3列 | 名前と順序が一致するか |
| 日本語 | 商品名・カテゴリ名 | 同じ文字 | 文字化けしていないか |
| インデックス列 | 保存対象外 | Unnamed: 0なし |
index=Falseが反映されたか |
source_for_export = df[shared_columns]
loaded = pd.read_csv(shared_path, encoding="utf-8-sig")
check_result = pd.DataFrame({
"確認項目": ["行数", "列数", "列名", "Unnamed: 0", "商品名"],
"結果": [
len(source_for_export) == len(loaded),
source_for_export.shape[1] == loaded.shape[1],
source_for_export.columns.tolist() == loaded.columns.tolist(),
"Unnamed: 0" not in loaded.columns,
source_for_export["商品"].tolist() == loaded["商品"].tolist(),
],
})
print(check_result)
print("再読み込み後の型:")
print(loaded.dtypes)
| 確認項目 | 結果 | |
|---|---|---|
| 0 | 行数 | True |
| 1 | 列数 | True |
| 2 | 列名 | True |
| 3 | Unnamed: 0 | True |
| 4 | 商品名 | True |
| dtype | ||||||
|---|---|---|---|---|---|---|
| 商品 | o | b | j | e | c | t |
| カテゴリ | o | b | j | e | c | t |
| 売上 | i | n | t | 6 | 4 |
5項目がすべてTrueなら、今回の目的に必要な行・列・日本語・インデックス設定は一致しています。
ただし、CSVはDataFrameの型情報を完全に保存する形式ではありません。日付、カテゴリ型、先頭に0を含むコードなどは、再読み込み時に想定と異なる型になることがあります。ファイルを次の分析へ使う前に、dtypesやinfo()を再確認してください。
よくある失敗を現象から確認・修正する
| 起きる現象 | 主な原因 | 確認する箇所 | 修正方針 | 修正後の確認 |
|---|---|---|---|---|
| 保存したファイルが見つからない | 相対パスの基準を誤解 | 現在の作業場所、resolve()、ファイル一覧 |
保存先を明示する | exists()がTrue |
| 存在しないフォルダへ保存できない | 親フォルダがない | 保存先の親フォルダ | 先にフォルダを作る | 目的のファイルがある |
Unnamed: 0が増える |
名前のないindexも保存した | 保存時のindexと再読込後の列名 |
不要ならindex=Falseで保存し直す |
列一覧から消える |
| 日本語が文字化けする | 保存時と読み手の文字コードが合わない | encodingと開くアプリ |
用途に合う文字コードで保存し直す | 商品名を正しく読める |
| 以前のファイルが消える | 同名へ既定モードで上書き | 保存先と同名ファイルの有無 | 別名またはmode="x" |
意図したファイルが両方ある |
| 追記の途中に列名が入る | mode="a"でheaderも出力 |
追記前後の先頭・末尾 | 追記時だけheader=False |
列名は先頭の1行だけ |
| 追記後に列がずれる | 列順・列数・区切りが不一致 | 既存CSVと追加DataFrameの列 | 列をそろえる。難しければ結合後に新規保存 | 再読込後の各値が正しい列にある |
存在しない保存先フォルダを直す
起きる現象
ファイル名は正しいのに、存在しない親フォルダを含むパスへ保存しようとしてエラーになります。
原因と確認方法
to_csv()は、指定した親フォルダを自動では作りません。parent.exists()で確認します。
修正方法
必要なフォルダを作ってから保存します。
nested_path = output_dir / "reports" / "sales_report.csv"
print("修正前に親フォルダがあるか:", nested_path.parent.exists())
nested_path.parent.mkdir(parents=True, exist_ok=True)
df.to_csv(nested_path, index=False)
print("修正後にファイルがあるか:", nested_path.exists())
修正前に親フォルダがあるか: False 修正後にファイルがあるか: True
修正前の親フォルダはFalse、作成して保存した後のファイルはTrueになります。自分のデータでは、フォルダ名のスペルやGoogle Driveのマウント先も同時に確認してください。
上書きと追記はmodeで区別する
通常の保存は、同名ファイルがあると上書きします。既存ファイルを残すならmode="x"、既存CSVの末尾へ行を追加するならmode="a"を使います。mode="a"は値の更新や列の追加ではありません。
| 目的 | mode | 注意点 |
|---|---|---|
| 通常保存・上書き | 既定の"w" |
同名ファイルの内容は置き換わる |
| 上書きを防いで新規保存 | "x" |
同名ファイルがあるとFileExistsError |
| 既存CSVへ行を追記 | "a" |
列順をそろえ、通常はheader=False |
protected_path = output_dir / "sales_protected.csv"
df.to_csv(protected_path, index=False)
try:
df.to_csv(protected_path, index=False, mode="x")
except FileExistsError:
print("同名ファイルがあるため、上書きを停止しました:", protected_path.name)
print("既存ファイルは残っているか:", protected_path.exists())
同名ファイルがあるため、上書きを停止しました: sales_protected.csv 既存ファイルは残っているか: True
FileExistsErrorが出たら、別名で保存するか、本当に上書きしてよいか判断します。続いて、列名と順序が同じデータを行方向へ追記する例を確認します。
append_path = output_dir / "sales_append.csv"
base = df[shared_columns].iloc[:3]
new_row = df[shared_columns].iloc[3:]
base.to_csv(append_path, index=False, encoding="utf-8-sig")
new_row.to_csv(
append_path,
index=False,
encoding="utf-8-sig",
mode="a",
header=False,
)
appended = pd.read_csv(append_path, encoding="utf-8-sig")
print(appended)
print("追記後の行数:", len(appended))
| 商品 | カテゴリ | 売上 | |
|---|---|---|---|
| 0 | ノート | 文具 | 1200 |
| 1 | ペン | 文具 | 900 |
| 2 | ファイル | 収納 | 1500 |
| 3 | 付箋 | 文具 | 600 |
追記後の行数: 4
追記後は4行で、列名は先頭の1回だけです。header=Falseを忘れると途中に列名が入ります。列構成が違う場合は追記せず、DataFrameを結合してから新しいCSVとして保存します。
Google Colabで保存するときの注意
この記事の相対パスは、Colabではランタイムの一時領域へ保存されます。ランタイム終了後も残したい場合は、Google Driveをマウントし、実在するDrive内フォルダの絶対パスを指定します。手元のPCへダウンロードする操作と、Driveへ保存する操作は別です。
Driveの接続、パスのコピー、CSVの再読み込みは、Google ColabでCSVを読み込む方法で詳しく確認してください。この記事ではto_csv()の設定と保存結果の検算に集中します。
自分のDataFrameへ置き換える前のチェックリスト
- DataFrame名は正しいか
- 保存する列名と順序を
df.columnsで確認したか - 現在のindexは単なる連番か、意味のあるIDか
- 保存前の行数・列数を
df.shapeで記録したか - 欠損値を空欄として出力してよいか
- 親フォルダは実在するか
- 同名ファイルを上書きしてよいか
- CSVを開くアプリと文字コードは合っているか
- 追記する場合、既存CSVと列名・順序・区切り文字が同じか
- 保存後にread_csv()で読み込み直したか
Excelから読み込んだデータをCSVへ変換する場合は、先にpandas read_excel()の使い方でシート・見出し・必要列が正しいか確認してください。
保存したCSVを次の分析へつなげる
CSV保存は分析の終点とは限りません。別のNotebookで読み込み直し、データの状態を確認してから集計へ進めます。
reused = pd.read_csv(shared_path, encoding="utf-8-sig")
print("再利用するデータ:")
print(reused)
print("カテゴリ別の売上合計:")
print(reused.groupby("カテゴリ", as_index=False)["売上"].sum())
再利用するデータ:
| 商品 | カテゴリ | 売上 | |
|---|---|---|---|
| 0 | ノート | 文具 | 1200 |
| 1 | ペン | 文具 | 900 |
| 2 | ファイル | 収納 | 1500 |
| 3 | 付箋 | 文具 | 600 |
カテゴリ別の売上合計:
| カテゴリ | 売上 | |
|---|---|---|
| 0 | 収納 | 1500 |
| 1 | 文具 | 2700 |
再読み込みしたデータから、カテゴリ別の売上合計を計算できました。保存後は、まず列・型・欠損を確認し、その後にpandas groupby・aggの使い方で複数列・複数集計へ進めます。
CSVを読み込み直したときに列名や型が想定と違う場合は、集計へ進む前に原因を修正してください。
まとめ
- DataFrameのCSV保存には
to_csv()を使う - 最初に保存先を決め、index・文字コード・保存列・上書きの順で判断する
- 連番インデックスが不要なら
index=Falseを使う - 意味のあるIDをインデックスにしている場合は、消す前に通常列へ戻すか判断する
- 日本語の文字コードは開く環境で選び、Excelで直接開く用途では
utf-8-sigを候補にする columnsで保存範囲を絞り、headerとsepは必要な仕様がある場合だけ変更する- 上書きを防ぐなら
mode="x"、行を追記する場合だけmode="a"を使う - 保存後はread_csv()で読み込み直し、行数・列名・日本語・不要列を検算する
最小コードを実行して終わりにせず、保存されたCSVを次の環境で正しく読めるかまで確認することが、失敗を防ぐポイントです。
関連記事
- Google ColabでCSVを読み込む方法|アップロード・Drive・read_csv
- Pandasデータ前処理の基本|読み込み後に確認する順番と欠損・型・重複の選び方
- pandas info()の使い方|Non-Null Count・データ型・欠損値の見方
- pandas read_excel()の使い方|Excel読み込み・sheet_name・header・usecolsを初心者向けに解説
- pandas select_dtypes()の使い方|データ型で列を選ぶ方法を初心者向けに解説
- Pandas DataFrameとは?作り方・基本操作・Seriesとの違いを初心者向けに解説
- pandas groupby・aggの使い方|複数列・複数集計と結果の見方
to_csv()でindex=Falseを付けるのはなぜですか?
0から始まる連番インデックスがCSVへ追加され、再読み込み時にUnnamed: 0となるのを防ぐためです。ただし、顧客IDなど意味のあるインデックスは、通常列へ戻して残すか判断してください
CSVをExcelで開くと文字化けする場合はどうしますか?
保存時のencodingと、Excelがどの文字コードとして開いているかを確認します。日本語をExcelで直接開く用途ではencoding="utf-8-sig"を候補にし、実際の利用環境で表示を確認してください。受け渡し先に指定がある場合は、その指定を優先します。
to_csv()で保存したファイルはどこにありますか?
相対パスなら現在の作業場所を基準に保存されます。Path("ファイル名").resolve()で絶対パスを確認できます。Colabの一時領域はランタイム終了後に消えるため、残したい場合はGoogle Driveなどへ保存します。
CSVへ追記するときにヘッダーが重複するのはなぜですか?
mode="a"で追記するときも、既定では列名が出力されるためです。既存CSVと列構成を確認し、行を追記するコードではheader=Falseを指定します。
CSVへ保存するとデータ型や日付形式は保持されますか?
完全には保持されません。CSVは文字で表現されるため、再読み込み時に型が推定されます。先頭の0を持つコード、日付、カテゴリ型、欠損値などは、読み込み後にdtypesやinfo()で確認してください。
Google Colab終了後もCSVを残すにはどうしますか?
Google DriveをマウントしてDrive内の実在パスへ保存するか、ランタイム終了前に端末へダウンロードします。Driveの接続とパス確認は関連記事を参照してください。
保存後に正しく出力できたか確認するにはどうしますか?
read_csv()で読み込み直し、保存前後のshape、列名と順序、日本語、欠損値、Unnamed: 0の有無を比べます。重要なデータでは数行だけでなく、行数や集計値も確認してください。
コメント