Python pandas 3.0対応 データ前処理チートシート
pandasでデータを扱うときによく使う、確認・欠損値処理・重複削除・型変換・抽出・集計・出力を、実務で使う順番にまとめます。
ExcelやCSVの集計、業務データの整理、機械学習へ渡す前のクリーニングに使える内容です。コードはpandas 3.0系を基準にし、2.xでも使いやすい書き方にしています。
import pandas as pd
import numpy as np
print(pd.__version__)
前処理の基本手順
迷ったら、次の順番で進めます。
- 元データを壊さないようコピーする
- 行数・列名・型・欠損を確認する
- 列名と空文字を整理する
- 重複を確認する
- 数値・日付・文字列の型を直す
- 変換できなかった値を確認する
- 必要な行・列を抽出する
- 列追加・名寄せ・集計を行う
- 件数や一意性を検証して出力する
「変換できたか」だけでなく、「変換に失敗した値は何か」「処理前後で行数がどう変わったか」まで確認するのがポイントです。
テストデータ
この記事では次のDataFrameを使います。
raw_df = pd.DataFrame({
"品番": ["A001", "A002", "A002", "A003", "A004"],
"品名": [" 標準ねじ ", "限定ボルト", "限定ボルト", "新型ナット", None],
"単価": ["120", "バルク品", "バルク品", "300", ""],
"数量": [10, 20, 20, None, 5],
"登録日": ["2026/01/05", "2026/01/06", "2026/01/06", "不明", "2026/01/09"],
})
# 元データを残して作業用DataFrameを作る
df = raw_df.copy()
CSV・Excelを読み込むときの注意
品番や郵便番号のように先頭の0が重要な列は、読み込み時に文字列型を指定します。読み込んだ後で文字列へ変換しても、すでに消えた0は戻りません。
df = pd.read_csv(
"input.csv",
dtype={"品番": "string"},
na_values=["", "N/A", "未設定"],
)
df = pd.read_excel(
"input.xlsx",
dtype={"品番": "string"},
)
文字コードが分かっているCSVでは、encodingも明示します。
df = pd.read_csv("input.csv", encoding="utf-8-sig")
1. データ全体を確認する
# 先頭・末尾
df.head()
df.tail()
# 行数・列数
df.shape
# 列名
df.columns.tolist()
# データ型と欠損状況
df.info()
# 基本統計量
df.describe()
df.describe(include="all")
列ごとの値と件数を見ると、表記ゆれや異常値に気づきやすくなります。
df["単価"].value_counts(dropna=False)
重いDataFrameでは、メモリ使用量も確認できます。
df.info(memory_usage="deep")
2. 列名と空文字を整理する
# 列名の前後にある空白を削除
df.columns = df.columns.str.strip()
# 列名を個別に変更
df = df.rename(columns={
"商品コード": "品番",
"登録日付": "登録日",
})
空文字や空白だけのセルは、そのままでは欠損値として数えられません。文字列セルの空白だけをpd.NAへ寄せます。
df = df.replace(r"^\s*$", pd.NA, regex=True)
ただし、空文字に業務上の意味があるデータでは一括置換せず、対象列を限定してください。
target_columns = ["品名", "単価", "登録日"]
df[target_columns] = df[target_columns].replace(r"^\s*$", pd.NA, regex=True)
3. 欠損値を確認・補完する
# 列ごとの欠損数
df.isna().sum()
# 列ごとの欠損率
df.isna().mean().sort_values(ascending=False)
# どこか1列でも欠損している行
df.loc[df.isna().any(axis=1)]
# 特定列が欠損している行
df.loc[df["品名"].isna()]
欠損行を削除する前に、削除条件と件数を確認します。
# 品番がない行だけ削除
before_rows = len(df)
df = df.dropna(subset=["品番"])
print(f"削除行数: {before_rows - len(df)}")
補完するときは、列の意味に合う値を使います。
df["数量"] = df["数量"].fillna(0)
df["品名"] = df["品名"].fillna("未設定")
平均値や0で機械的に埋めると集計結果が変わることがあります。「不明」と「0」は同じではないため、補完ルールを先に決めておきます。
4. 重複を確認・削除する
# 行全体が重複している行
df.loc[df.duplicated(keep=False)]
# 品番が重複している行をすべて表示
df.loc[df.duplicated(subset=["品番"], keep=False)]
# 完全に同じ行だけ削除
df = df.drop_duplicates()
# 品番ごとに最後の1件を残す
df_latest = df.drop_duplicates(subset=["品番"], keep="last")
keep=Falseを使うと、重複グループの先頭行も含めて確認できます。いきなり削除せず、どの列を重複判定のキーにするかを決めるのが安全です。
5. 型を確認・変換する
df.dtypes
# pandasのnullable型へ変換できる列をまとめて変換
df = df.convert_dtypes()
pandas 3.0の文字列型
pandas 3.0では、文字列データの既定型が従来のobjectから専用のstr dtypeへ変わりました。一方、convert_dtypes()やastype("string")で使うnullableなStringDtypeも引き続き利用できます。
2.xと3.xの両方を意識するなら、文字列列をdtype == "object"だけで判定しないようにします。
from pandas.api.types import is_string_dtype
is_string_dtype(df["品名"].dtype)
欠損を保った文字列列として明示したい場合は、次の書き方が分かりやすいです。
df["品番"] = df["品番"].astype("string")
df["品名"] = df["品名"].astype("string").str.strip()
数値変換と変換エラーの確認
errors="coerce"は、数値に変換できない値を欠損値にします。ただし、変換後にisna()だけを見ると「元から空だった値」と「変換に失敗した値」が混ざります。
変換前のSeriesを残して比較します。
price_source = (
df["単価"]
.astype("string")
.str.replace(",", "", regex=False)
.str.replace("円", "", regex=False)
.str.strip()
)
price_converted = pd.to_numeric(price_source, errors="coerce")
# 値は入っていたが、数値へ変換できなかった行
invalid_price = df.loc[
price_source.notna() & price_converted.isna(),
["品番", "品名", "単価"],
]
df["単価"] = price_converted
この例ではバルク品がinvalid_priceに入ります。元から空だった単価とは区別できます。
日付変換と変換エラーの確認
形式が決まっている日付はformatを指定すると、意図が明確になります。
date_source = df["登録日"].astype("string").str.strip()
date_converted = pd.to_datetime(
date_source,
format="%Y/%m/%d",
errors="coerce",
)
invalid_date = df.loc[
date_source.notna() & date_converted.isna(),
["品番", "登録日"],
]
df["登録日"] = date_converted
変換できない日付はNaTになります。日付形式が複数混在している場合は、まず形式ごとの件数を調べてからルールを決めます。
6. 条件で行・列を抽出する
# 単価が100以上
df.loc[df["単価"].ge(100)]
# 複数条件。各条件を丸括弧で囲む
condition = df["単価"].ge(100) & df["数量"].ge(10)
df.loc[condition.fillna(False), ["品番", "品名", "単価", "数量"]]
# リストに含まれる品番
target_items = ["A001", "A003"]
df.loc[df["品番"].isin(target_items)]
# 必要な列だけ取り出す
df.loc[:, ["品番", "品名", "単価"]]
Pythonのand・orではなく、pandasでは&・|を使います。欠損値を含む条件は、抽出方針に応じてfillna(False)などで明示します。
7. 文字列を加工する
# 前後の空白を削除
df["品名"] = df["品名"].astype("string").str.strip()
# 「限定」を含む行。欠損値はFalse扱い
df.loc[df["品名"].str.contains("限定", na=False)]
# 品番の先頭2文字
df["品番_prefix"] = df["品番"].str[:2]
# 正規表現で数字部分を抽出
df["品番数字"] = df["品番"].str.extract(r"(\d+)", expand=False)
正規表現ではなく固定文字列を探す場合は、regex=Falseを指定すると意図が明確です。
df["品名"].str.contains("[限定]", regex=False, na=False)
8. 条件に応じて列を追加する
nullable型では、比較結果にpd.NAが含まれることがあります。np.whereへ渡す前に欠損時の扱いを決めます。
high_price = df["単価"].ge(200).fillna(False)
df["高単価"] = np.where(high_price, "高い", "通常")
文字列条件で分類するときは、単純な条件ならlocで十分です。
name = df["品名"].astype("string")
df["製品カテゴリ"] = "通常品"
df.loc[name.isna(), "製品カテゴリ"] = "未設定"
df.loc[name.str.contains("限定", na=False), "製品カテゴリ"] = "限定品"
df.loc[name.str.contains("新", na=False), "製品カテゴリ"] = "新製品"
複数条件に一致する場合、後に代入した値が残ります。条件の優先順位をコメントや仕様書に残しておきます。
9. 対応表を使って名寄せする
1列の値を置き換えるだけならmap、別表から複数列を付けるならmergeが便利です。
category_map = {
"A001": "標準品",
"A002": "特注品",
}
df["区分"] = df["品番"].map(category_map).fillna("未分類")
master = pd.DataFrame({
"品番": ["A001", "A002", "A003"],
"担当部署": ["営業1課", "営業2課", "開発課"],
})
df = df.merge(
master,
on="品番",
how="left",
validate="many_to_one",
)
validate="many_to_one"を付けると、マスター側の品番が重複している場合にエラーになります。意図しない行数増加の検知に役立ちます。
10. グループ化して集計する
summary = (
df.groupby("製品カテゴリ", as_index=False)
.agg(
数量合計=("数量", "sum"),
平均単価=("単価", "mean"),
行数=("品番", "size"),
品番あり件数=("品番", "count"),
)
)
-
size: 欠損値に関係なくグループの行数を数える -
count: 指定列の欠損値を除いて数える
単純な合計だけなら、次のようにも書けます。
df.groupby("製品カテゴリ")["数量"].sum()
11. 縦持ち・横持ちを変換する
Excelのクロス集計のような表を作るならpivot_table、列を縦へほどくならmeltを使います。
pivot = pd.pivot_table(
df,
index="製品カテゴリ",
values="数量",
aggfunc="sum",
fill_value=0,
)
long_df = df.melt(
id_vars=["品番", "品名"],
value_vars=["単価", "数量"],
var_name="項目",
value_name="値",
)
12. 数値を区間に分ける
pd.cutで単価帯などの区間を作れます。
bins = [-np.inf, 100, 200, 300, np.inf]
labels = ["100未満", "100以上200未満", "200以上300未満", "300以上"]
df["単価帯"] = pd.cut(
df["単価"],
bins=bins,
labels=labels,
right=False,
)
right=Falseでは、100は「100以上200未満」に入ります。
13. 処理結果を検証する
出力前に、最低限の条件をassertで確認できます。
# 品番がすべて入っている
assert df["品番"].notna().all(), "品番が空の行があります"
# 品番が一意であるべきデータの場合
assert df["品番"].is_unique, "品番が重複しています"
# 数量がマイナスでない
assert df["数量"].dropna().ge(0).all(), "数量に負数があります"
仕様上重複が正しいデータにis_uniqueを使う必要はありません。業務ルールに合った検証だけを入れます。
14. CSV・Excelへ出力する
df.to_csv(
"output.csv",
index=False,
encoding="utf-8-sig",
)
df.to_excel(
"output.xlsx",
index=False,
)
WindowsユーザーとMacユーザーで共有するときの文字コード
utf-8-sigは、先頭にBOM(Byte Order Mark)が付いたUTF-8です。Microsoftは、BOM付きのUTF-8 CSVはExcelで通常どおり開けると案内しています。そのため、Windows版Excelでダブルクリックして開く人がいるCSVでは、utf-8-sigが無難です。
Mac版Excelにも「CSV UTF-8」形式があります。WindowsとMacの間で日本語CSVを共有する場合も、まずはUnicode文字を広く扱えるutf-8-sigを候補にします。
| 用途 | pandasの指定 | 注意点 |
|---|---|---|
| Windows版Excelで直接開く | encoding="utf-8-sig" |
BOMによりUTF-8として認識されやすい |
| Mac版ExcelやWindows版Excelとの共有 | encoding="utf-8-sig" |
現在のExcel同士で共有しやすい |
| Web、API、データベース、Linux系ツール | encoding="utf-8" |
BOMなしUTF-8を要求する仕様が多い |
| 古い日本語Windows向けシステム | encoding="cp932" |
Windows依存。MacやWebとの共有には不向き |
# Windows / MacのExcelで開く共有用CSV
df.to_csv("output.csv", index=False, encoding="utf-8-sig")
# BOMなしUTF-8を指定されたシステム連携用CSV
df.to_csv("output_utf8.csv", index=False, encoding="utf-8")
# 取込仕様でCP932を明示された場合のみ
df.to_csv("output_cp932.csv", index=False, encoding="cp932")
cp932では、絵文字や一部の漢字などを表現できず、書き出し時にUnicodeEncodeErrorになることがあります。また、Mac版とWindows版でCSVの既定動作や選べる保存形式が異なる場合があります。
したがって、文字コードは「相手がWindowsかMacか」だけで決めず、次の順で判断します。
- 取込先システムの仕様書に指定があれば従う
- 人がExcelで開く共有ファイルなら
utf-8-sigを試す - WebやAPI連携でBOMなしを求められたら
utf-8を使う - 古いWindowsシステムがCP932を要求するときだけ
cp932を使う
文字化けしたCSVをExcelで開く場合は、ダブルクリックではなく「データ」からテキスト/CSVとして取り込み、文字コードを指定する方法もあります。
最初に実行する確認セット
print("shape:", df.shape)
print("duplicated rows:", df.duplicated().sum())
print(df.dtypes)
display(df.head())
display(df.isna().sum().sort_values(ascending=False))
display(df.describe(include="all"))
Jupyter Notebook以外のPythonスクリプトでは、display()の代わりにprint()を使います。
まとめ
- 品番などは読み込み時に文字列型を指定する
- 空文字と欠損値は同じ扱いにするか、列ごとに決める
- 重複は
keep=Falseで削除前に全件確認する - 型変換前の値を残し、変換失敗と元の欠損を区別する
- pandas 3.0では文字列型の既定値が変わったため、
object決め打ちを避ける - nullable型の条件式では
pd.NAの扱いを明示する -
merge(validate=...)やassertで、行数増加・重複・欠損を検知する
前処理では、コードを短くすることより「何を欠損とみなしたか」「何件削除・変換したか」を後から説明できることが大切です。