Pythonデータ分析 #4 変形と欠損値 — 新しい列・日付・空欄の扱い方

読了 8分

CSV を読み込んだ瞬間からすぐ分析できるデータは、ほとんどありません。文字列の前後に空白が付いていて、日付が文字列のまま入っていて、あちこちが空いていて、同じ行が 2 回入っています。実務では、分析時間の半分以上がこの整理作業に費やされます。前回で欲しい行と列を選び出す方法を身につけたので、今回は選び出したデータを分析できる形に変える作業を扱います。サンプルデータには、わざと空白・欠損・重複を混ぜてあります。

サンプルデータ
import pandas as pd
import numpy as np

df = pd.DataFrame({
    "注文番号": [1001, 1002, 1003, 1004, 1004],
    "商品": ["  キーボード", "マウス", "USBハブ", "モニター", "モニター"],
    "単価": [49000, 15000, None, 320000, 320000],
    "数量": [2, 1, 3, 1, 1],
    "注文日": ["2026-04-01", "2026-04-03", "2026-04-05", "2026-04-10", "2026-04-10"],
    "クーポン": ["SUMMER10", None, "SUMMER10", None, None],
})

新しい列を作る: 演算1行で済みます #

単価と数量を掛けた金額列を作ってみます。for ループは必要ありません。

列同士の演算
df["金額"] = df["単価"] * df["数量"]
# 0     98000.0
# 1     15000.0
# 2         NaN
# 3    320000.0
# 4    320000.0

df["単価"] * df["数量"] は、列全体を一度に掛け算します。これが ベクトル化 という考え方です。行を 1 つずつ回る Python のループの代わりに、pandas が内部の C コードで列全体を一度に処理するので、100万行でも同じ 1 行です。「行ごとにどう処理するか」ではなく「列同士でどんな演算をするか」へ発想を切り替えることが、pandas に慣れる上での核心です。欠損があった行は結果も NaN です。欠損は演算を経ても欠損のまま残るので、後ほど整理します。

applyは最後の手段です #

条件が入ると、apply に関数を渡したくなります。

apply(遅い方法)
def shipping(row):
    if row["金額"] >= 100000:
        return 0
    return 3000

df["送料"] = df.apply(shipping, axis=1)  # 行ごとに呼び出される

動きはしますが、apply は行ごとに Python の関数を呼び出します。ベクトル化が崩れて事実上 Python のループになり、100万行なら 100万回の関数呼び出しです。同じことをベクトル演算で書くとこうなります。

np.where(速い方法)
df["送料"] = np.where(df["金額"] >= 100000, 0, 3000)

np.where(条件, 真のときの値, 偽のときの値) が列全体に一度に適用され、データが大きくなるほど差は数十倍から数百倍まで開きます。それでも apply を使うのは、ベクトル演算では表現しにくい複雑なロジックや、外部ライブラリの関数を行ごとに呼び出す必要がある場面くらいです。ベクトル演算で書けるならベクトル演算で書き、apply は最後に取り出します。

文字列処理: strアクセサ #

文字列の列には .str を付けて、Python の文字列メソッドを列全体に適用します。

strアクセサ
df["商品"] = df["商品"].str.strip()                # 前後の空白を除去
df["クーポン"] = df["クーポン"].str.replace("10", "")  # 置換
df["商品"].str.split(" ").str[0]                   # 分割して最初の要素

特に strip は、ほとんどすべての実務データで一度は使うことになります。「キーボード」と「 キーボード」は目には同じに見えても別の値なので、空白を整理しないと、次回扱うグループ集計が 2 つに分かれてしまいます。

日付処理: to_datetimeとdtアクセサ #

いま注文日の列はただの文字列です。pd.to_datetime で本物の日付型(datetime64)に変換すると、.dt アクセサで年・月・曜日をすぐ取り出せます。

日付変換とdtアクセサ
df["注文日"] = pd.to_datetime(df["注文日"])
df["曜日"] = df["注文日"].dt.day_name()
# 0    Wednesday
# 1       Friday
# 2       Sunday

dt.yeardt.monthdt.day も同じ要領です。日付同士を引くと期間が出ます。df["経過日数"] = (pd.Timestamp("2026-04-15") - df["注文日"]).dt.days のように基準日から引いて .dt.days で日数だけ取り出すと、14、12、10、5、5 になります。

欠損値の把握: NaNの正体 #

空いている値は、pandas では NaN と表示されます。Not a Number の略で、もともとは浮動小数点の標準規格にある特殊な float 値です。そのため整数の列に欠損が 1 つでも生じると、列全体が float に変わります。先ほど金額が 98000.0 のように小数点付きで見えた理由です。NaN には奇妙な性質も 1 つあります。np.nan == np.nan が False になるほど、自分自身とも等しくありません。そのため == の比較では欠損を見つけられず、専用メソッドの isna を使います。

欠損の集計
df.isna().sum()
# 単価      1
# クーポン    3
# 残りの列はすべて 0

isna() は欠損かどうかを True と False で返し、sum() が True を 1 と数えるので、列ごとの欠損数が出ます。新しいデータを受け取ったら、まず最初に実行する 1 行です。

埋めるか消すか: dropnaとfillna #

欠損処理は、消すか埋めるかの 2 択です。

dropnaとfillna
df.dropna()                  # 欠損が1つでもある行をすべて除去
df.dropna(subset=["単価"])   # 単価が空の行だけ除去
df["クーポン"] = df["クーポン"].fillna("未使用")   # 欠損を指定した値で埋める

どちらを選ぶかは、その空欄の意味にかかっています。クーポンの欠損は「クーポンを使わなかった」という情報なので、「未使用」で埋めるのが正解です。一方、単価の欠損は値がわからないということなので、0 で埋めると平均単価も合計金額もすべて歪みます。0 と欠損は別の値 です。0 は「値が 0 である」という事実で、欠損は「値がわからない」という状態です。平均で埋める方法もよく見かけますが、平均代入はデータのばらつきを実際より小さくし、欠損が多いほど分布が平均側に寄ります。欠損率が低く、単純な要約が目的のときだけ軽く使い、それ以外は消すほうが安全な場合が多いです。

型変更: astype #

単価を整数に戻したくても、NaN が残っている間は整数への変換がエラーになります。NaN 自体が float なので、整数の列には入れられないからです。欠損を整理してから変換します。

astype
df = df.dropna(subset=["単価"])
df["単価"] = df["単価"].astype("int64")       # float64 → int64
df["商品"] = df["商品"].astype("category")  # カテゴリ型

値の種類が数パターンに決まっている文字列の列は、最後の行のようにカテゴリ型に変えておくのがおすすめです。同じ文字列を繰り返し保存する代わりに、内部で番号を振って保存するため、メモリが大きく減り、グループ集計も速くなります。数百万行に商品名が数十種類しかないデータなら、効果は確実です。

重複の除去: duplicatedとdrop_duplicates #

注文番号 1004 が 2 回入っていました。duplicated で確認し、drop_duplicates で除去します。

重複の除去
df.duplicated().sum()        # 1
df = df.drop_duplicates()

duplicated は、すべての列の値が完全に同じ行を重複と見なします。特定の列だけで判定するなら subset=["注文番号"] のように指定し、デフォルトでは最初の行を残しますが、keep="last" で最後の行を残すこともできます。重複に気づかないまま合計を出すと売上が水増しされるので、欠損の集計と合わせて、分析の序盤に一度確認する習慣が必要です。

まとめ #

今回扱った内容です。

  • 新しい列は列同士の演算 1 行で作り、行ループではなくベクトル化で考えます
  • apply は行ごとに Python の関数を呼び出して遅いので、最後の手段として残します
  • .str アクセサで空白除去・置換・分割を列全体に適用します
  • to_datetime で日付型を作り、.dt で年・月・曜日・日付の差を取り出します
  • 欠損は isna().sum() で把握し、空欄の意味に応じて dropnafillna を選びます
  • astype で型を整理し、繰り返しの文字列はカテゴリ型に変え、drop_duplicates で重複行を除去します

次回(#5 グループ・集計・結合)では、整理したデータを実際に要約する段階へ進みます。groupby でグループごとの合計と平均を出し、複数の表を mergeconcat でつなぎ合わせる方法まで扱います。

X