113
144

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python pandas 3.0対応 データ前処理チートシート(欠損値・型変換・重複・抽出・集計)

113
Last updated at Posted at 2020-06-12

Python pandas 3.0対応 データ前処理チートシート

pandasでデータを扱うときによく使う、確認・欠損値処理・重複削除・型変換・抽出・集計・出力を、実務で使う順番にまとめます。

ExcelやCSVの集計、業務データの整理、機械学習へ渡す前のクリーニングに使える内容です。コードはpandas 3.0系を基準にし、2.xでも使いやすい書き方にしています。

import pandas as pd
import numpy as np

print(pd.__version__)

前処理の基本手順

迷ったら、次の順番で進めます。

  1. 元データを壊さないようコピーする
  2. 行数・列名・型・欠損を確認する
  3. 列名と空文字を整理する
  4. 重複を確認する
  5. 数値・日付・文字列の型を直す
  6. 変換できなかった値を確認する
  7. 必要な行・列を抽出する
  8. 列追加・名寄せ・集計を行う
  9. 件数や一意性を検証して出力する

「変換できたか」だけでなく、「変換に失敗した値は何か」「処理前後で行数がどう変わったか」まで確認するのがポイントです。

テストデータ

この記事では次の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のandorではなく、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か」だけで決めず、次の順で判断します。

  1. 取込先システムの仕様書に指定があれば従う
  2. 人がExcelで開く共有ファイルならutf-8-sigを試す
  3. WebやAPI連携でBOMなしを求められたらutf-8を使う
  4. 古い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で、行数増加・重複・欠損を検知する

前処理では、コードを短くすることより「何を欠損とみなしたか」「何件削除・変換したか」を後から説明できることが大切です。

公式ドキュメント

113
144
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
113
144

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?