0
0

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ライブラリ応用(上級編)

0
Last updated at Posted at 2026-05-25

この文章は、以前受講したデータ分析コースで取ったメモと、小売業界でのデータ分析を基に作成されています。

EDA(Exploratory Data Analysis:探索的データ分析)では、データの理解・可視化・前処理を行うために、Pythonの代表的なライブラリであるNumPy、Pandas、Matplotlibの知識が必須となる。前処理、集計、可視化の工夫など、実際に手を動かす場面で差が出やすい内容に絞っています。
NumPyは、Pythonにおける数値計算の基盤となるライブラリであり、高速な多次元配列を提供する。
Pandasは、構造化データを扱うためのDataFrameを提供するデータ分析ライブラリである。
Matplotlibは、グラフや図を作成するための可視化ライブラリである。

参考として、次の記事も参照してください。

データサイエンティストに必要な最低限のPython知識
データサイエンティストのためのPythonライブラリ応用(入門編)
データサイエンティストのためのPythonライブラリ応用(中級編)

導読

本文は、「読み込み -> 前処理 -> 集計 -> 可視化 -> 検索と結合 -> 日付処理」という実務の流れに沿って、高度な書き方を追加するためのメモです。中級編の二級見出しをそのまま土台にしているので、まず中級で全体像をつかみ、次にこの文章で同じ場所の高度な操作へ進む、という読み方がしやすくなっています。

  1. データ読み込み
    ndarray の器の見方、dtype、再現性、検証用サンプル作成、CSV / pickle / 日時列の読み込み、他ライブラリへ渡す形式変換を扱います。
  2. データ前処理
    配列形状の変換、ブロードキャスト、条件分岐のベクトル化、欠損・重複処理、補完戦略、ラベル整形、カテゴリ型までを段階的に整理します。
  3. データ集計
    統計量の読み取り、頻度確認、groupby() の発展形、crosstab() / pivot_table() の使い分け、MultiIndex の扱い方までを扱います。
  4. データ可視化
    Pandas の簡易描画から Matplotlib の細かな調整、Seaborn の多変量可視化、仕上げのレイアウト制御までをひと続きで確認します。
  5. データ検索・結合
    並べ替え、抽出、インデックス操作、concat()merge() を通じて、複数表を安全につなぐ考え方を整理します。
  6. 日付データの処理
    日時列の生成、DatetimeIndexresample()shift()、時系列可視化までをまとめ、時間軸を持つデータへ拡張します。

目標は、「どのライブラリの関数か」だけでなく、「この工程ならこの書き方が自然だ」と判断しながら、分析コード全体を自力で組み立てられるようになることです。

実務で特に使用頻度が高い部分を二八法則で見るなら、まず重点的に押さえるべきなのは read_csv()isnull().sum()dropna() / fillna()groupby()pivot_table()plot() / histplot() / scatterplot()merge()to_datetime()resample()shift() です。

小売や EC では、売上 CSV を読む、欠損を確認する、店舗やカテゴリ単位で集計する、施策前後を図で比べる、顧客台帳と注文台帳を結合する、日次データを週次や月次へまとめる、といった流れが何度も出てきます。最初は全項目を均等に覚えるより、この高頻度部分を繰り返し使って「読める・直せる・少し変えられる」状態を作る方が効果的です。


データ読み込み

この章では、外部ファイルを読む前に配列の器と再現性を決め、読み込んだ結果を次の処理へ渡しやすい形へ整えます。読み込みの段階で型や形を曖昧にすると、その後の前処理と可視化で「なぜか動かない」時間が増えやすいので、最初に足場を固めます。

読み込み前の土台を整える

この節の目的は、「どんな入れ物に、どんな型で、どんな再現条件でデータを置くか」を先に決めることです。ここが曖昧だと、後から欠損処理や reshape をするときに毎回立ち止まることになります。

Python list と ndarray の違いを最初に押さえる

list は要素の追加や連結には便利だが、数値計算の主役としては使いにくい。一方 ndarray は型がそろい、要素ごとの演算や形状変換を前提にしているので、読み込み後の処理へ自然につながる。
初学者が最も混乱しやすいのは、「見た目はどちらも配列なのに、演算結果が違う」点である。最初にここを理解しておくと、その後の NumPy と Pandas のコードが読みやすくなる。
小売・EC の典型シーンでは、日別売上、在庫数、商品別単価のような数値列をあとで一括計算したいので、早い段階で ndarray 的な考え方に寄せておくと集計と可視化へつなぎやすい。

import numpy as np

# list は連結や繰り返しが中心で、数値演算の器ではない
li1 = [1, 3, 5]
li2 = [2, 4, 6]

# ndarray は同じ型の値をまとめて持ち、要素ごとの演算が自然に書ける
arr1 = np.array(li1)
arr2 = np.array(li2)

print(li1 * 2)
print(arr1 * 2)
print(li1 + li2)
print(arr1 + arr2)

# astype() を使うと、後段の計算に合わせて型をまとめて変換できる
arr3 = np.array([1.2, 2.8, 3.5])
print(arr3.astype(int))
print(arr3.astype(object).dtype)

dtype・ndim・shape・size で器を確認する

dtype は各要素の型、ndim は次元数、shape は各軸の長さ、size は全要素数を表す。どのライブラリへ渡すにしても、この 4 つを見れば「今の配列がどういう器か」を短時間で説明できる。
特に reshape(-1, 1)reshape(1, -1) は、列ベクトルと行ベクトルを素早く作る基本形である。scikit-learn や可視化関数に渡す直前によく使うので、早い段階で慣れておくとよい。
小売の店舗 x 日付、EC の商品 x 指標のような表を扱うときも、どの軸が店舗でどの軸が日付かを shapendim で説明できると、後続の前処理ミスが減る。

import numpy as np

# dtype を見ると、整数か浮動小数点かをすぐ確認できる
x = np.array([1, 2, 3, 4, 5], dtype="float32")
print(x.dtype)

# ndim / shape / size を並べて見ると、器の説明がしやすい
arr = np.random.rand(2, 3, 4)
print(arr.ndim)
print(arr.shape)
print(arr.size)

# -1 を使うと、残り次元を NumPy 側に自動計算させられる
base = np.arange(12)
print(base.reshape(-1, 1).shape)
print(base.reshape(1, -1).shape)
print(base.reshape(-1).shape)

配列生成関数で検証用データを準備する

実務では、いきなり本番データだけで試すより、小さな検証用配列で挙動を確認してから本番へ戻る方が速い。np.zeros()np.ones()np.full()np.eye()np.arange()np.linspace() はそのための基本セットである。
値の並びや型を自分で完全に制御できるので、ブロードキャストや欠損補完の確認コードを書くときにも役立つ。覚えるべきなのは関数名より、「どんな検証状況を最短で作れるか」である。
例えば EC の値引き率ロジックや、小売の棚割り計算を試すときも、まず小さい配列で期待通りに動くか確かめてから注文データ全体へ当てる方が安全である。

import numpy as np

# 初期値を持つ配列を作ると、演算結果の確認がしやすい
print(np.zeros(5, dtype=int))
print(np.ones((2, 4), dtype=float))
print(np.full((3, 5), 8.8))

# 単位行列は線形代数や対角成分の確認に便利である
print(np.eye(3))

# arange は等差列、linspace は区間を等分したいときに使う
print(np.arange(1, 15, 2))
print(np.linspace(0, 1, 4))

dtype を明示し、重み付きサンプリングで確認用サンプルを作る

np.random.choice() は、元データを壊さずに小さな検証サンプルを切り出したいときに便利である。p= を与えれば重み付きサンプリングもできるので、実務の偏りを簡単に再現できる。
同時に np.random.seed() を入れておくと、教材コードや検証コードの再現性が上がる。欠損補完や可視化を比較するときに毎回結果が変わると、判断基準がぶれやすいので、乱数の固定は地味だが重要である。
EC では購入頻度の高い会員ほど抽出されやすいサンプルを作って施策検証したいことがあり、p= を使った重み付き抽出はその発想を手早く試すときに役立つ。

import numpy as np

# 乱数シードを固定すると、確認用コードを何度実行しても同じ結果になる
np.random.seed(17)

# dtype を明示しておくと、後で平均や比率を取るときに扱いやすい
base = np.arange(10, 25, dtype=float)

# 単純サンプリング
sample1 = np.random.choice(base, size=(2, 3))

# p= を与えると、値の大きい要素を出やすくするような重み付き抽出もできる
sample2 = np.random.choice(base, size=(2, 3), p=base / base.sum())

print(sample1)
print(sample2)

# 順序を崩したいだけなら permutation / shuffle が簡潔である
y = np.arange(5)
print(np.random.permutation(y))
np.random.shuffle(y)
print(y)

# シードを再設定すると、同じ乱数列を再現できる
np.random.seed(17)
print(np.random.randint(1, 100))
np.random.seed(17)
print(np.random.randint(1, 100))

ファイルとメモリ形式を行き来する

この節では、CSV のような汎用形式と、pickle のような中間保存向け形式を行き来する感覚をつかみます。分析では「まず読む」「途中結果を保存する」「別ライブラリへ渡す」を何度も繰り返すので、形式変換に慣れておくと作業が止まりにくいです。

共通サンプルCSVを先に作ってから使う

この文では後半で Example_without_index.csvExample_with_index.csv を何度も再利用する。読者が途中で「その CSV はどこから来たのか」で止まらないように、先に小さな小売サンプルを自分で生成して保存しておく。
小売・EC の典型シーンでは、最初に小さな共通サンプルを用意しておくと、欠損、集計、可視化、結合の各節で同じ表を使い回せるので、コードの意味を追いやすい。特に学習用の文では、出所不明の CSV を読むより、自分で作った表を保存してから読む方が理解しやすい。

import pandas as pd

# 小売の売上明細をイメージした小さな共通サンプルを list から作る
rows = [
    ["2024-01-02", 120, 3, 2.4, 10.5, "red", "circle", "Tokyo", "drink"],
    ["2024-01-03", 85, 1, 1.8, 9.8, "blue", "square", "Osaka", "snack"],
    ["2024-01-04", 200, 5, None, 11.2, "green", "triangle", "Nagoya", "household"],
    ["2024-01-05", 95, 2, 2.1, None, "blue", "circle", "Tokyo", "daily"],
    ["2024-01-06", 150, 4, 2.9, 10.1, "red", "square", "Fukuoka", "cosmetics"],
    ["2024-01-07", 60, 1, 1.5, 8.7, None, "circle", "Osaka", "snack"],
    ["2024-01-08", 175, 3, None, 9.5, "green", "triangle", "Nagoya", "household"],
    ["2024-01-09", 130, 2, 2.2, 10.9, "blue", "square", "Tokyo", "drink"],
    ["2024-01-10", 220, 6, 3.1, None, "red", "circle", "Sapporo", "seasonal"],
    ["2024-01-11", 75, 1, 1.7, 8.9, "green", "square", "Fukuoka", "daily"],
]

columns = ["Date", "Price", "Quantity", "Width", "Height", "Color", "Shape", "Store", "Category"]
df = pd.DataFrame(rows, columns=columns)

# 後続の parse_dates 用に、文字列のまま CSV へ保存する
df.to_csv("Example_without_index.csv", index=False)

# index_col=0 の例でも使えるように、行ラベル付き版も保存する
df_with_index = df.copy()
df_with_index.index = [f"row_{i:02d}" for i in range(1, len(df_with_index) + 1)]
df_with_index.to_csv("Example_with_index.csv", index=True, index_label="row_id")

print(df.head())

read_csv() / read_pickle() / to_pickle() / parse_dates を組み合わせる

CSV は汎用性が高いが、日時列は parse_datespd.to_datetime() で明示的に整える必要がある。pickle は型情報を保持しやすく、中間成果物の保存に向く。
分析の初期段階では CSV を読むことが多いが、前処理後の途中結果まで毎回 CSV に戻すと、日時やカテゴリの情報が崩れやすい。重い前処理を繰り返したくないときは pickle が有効である。
小売・EC の典型シーンでは、生データは CSV、途中まで整えた特徴量テーブルは pickle、という使い分けにすると、毎回同じ前処理をやり直さずに済む。

import pandas as pd

# parse_dates を指定すると、Date 列を読み込み時点で datetime64 型に変換できる
df = pd.read_csv("Example_with_index.csv", index_col=0, parse_dates=["Date"])

# index=False は、保存時に行番号を余計な列として書き出さないための指定である
df.to_csv("Example_rewritten.csv", index=False)

# pickle は型情報を保ったまま中間データを保存しやすい
df.to_pickle("Example_rewritten.pkl")

# 読み戻し時に型が崩れていないかを確認する
df_pkl = pd.read_pickle("Example_rewritten.pkl")
print(df.dtypes["Date"])
print(df_pkl.dtypes.head())

Series / DataFrame を ndarray・list・dict に変換する

Pandas のままでは便利でも、NumPy・scikit-learn・JSON 出力などでは別形式へ変換したくなることが多い。valuestolist()to_dict() を使い分けられると、ライブラリ間の受け渡しがかなり楽になる。
重要なのは、「どの形式が次の処理に一番自然か」を意識することだ。数値計算なら ndarray、小さな確認なら list、API やログ出力なら dict が読みやすいことが多い。
例えば EC で推薦モデルへ価格配列を渡すときは ndarray、BI へ渡す直前の軽い確認なら list、API 応答やテストデータなら dict が扱いやすい。

import pandas as pd

df = pd.read_csv("Example_without_index.csv")

# values は NumPy 配列として取り出したいときに使う
print(df["Price"].values[:5])

# tolist() は小さな確認用の出力に向く
print(df["Price"].tolist()[:5])

# orient="records" は 1 行 1 辞書の形で外部連携しやすい
print(df[["Price", "Quantity"]].to_dict(orient="records")[:3])

データ前処理

この章では、「形を整える -> 条件で切り分ける -> 欠損と重複を処理する -> ラベルを整理する」という順に、後続の集計と可視化を楽にする準備をまとめます。前処理は単なる掃除ではなく、「このデータをどう解釈するか」をコードに落とす工程だと考えると理解しやすくなります。

配列形状と演算の基礎を整える

この節の目的は、配列の形を崩さずに演算を続けられるようにすることです。ここで shape の感覚を持てると、後半の groupby() や可視化にもつながります。

reshape と flatten / ravel の違い

reshape() は形状変更、flatten() はコピー、ravel() はビューを返す。つまり、見た目は似ていても「元配列とメモリを共有するか」が違う。
一時的に 1 次元へならすだけなら ravel() が軽いが、元配列と切り離したいなら flatten() の方が安全である。ここを理解しておくと、後で値を書き換えたときの事故を防ぎやすい。

import numpy as np

x = np.arange(12)

# 3 行 4 列へ形状を変える
matrix = x.reshape(3, 4)

# np.newaxis を使うと、列ベクトルの形も明示しやすい
column = x[:, np.newaxis]
print(matrix.shape)
print(column.shape)

x2 = np.random.randint(0, 10, (3, 4))

# flatten() はコピーなので、元配列とは独立している
flat = x2.flatten()

# ravel() はビューになりやすく、元配列の変更が反映されることがある
view = x2.ravel()

print(flat[0])
print(x2[0, 0])
print(view[:5])

配列の結合と分割

前処理では、特徴量を横に足したり、目的変数だけを切り出したりする操作が多い。hstack() / vstack() / np.c_hsplit() / vsplit() を押さえておくと、配列単位の前処理を短く書ける。
結合方向を意識せずに使うと shape エラーが出やすいので、「列を増やしたいのか、行を増やしたいのか」を先に言葉で説明してから書くとミスが減る。

import numpy as np

x1 = np.array([1, 2, 3])
x2 = np.array([4, 5, 6])

# hstack は列方向(横)に連結し、np.c_ は列を増やした 2 次元配列を作りやすい
print(np.hstack([x1, x2]))
print(np.c_[x1, x2])

x3 = np.arange(24).reshape(6, 4)

# hsplit / vsplit を使うと、説明変数と目的変数の分離を試しやすい
left, right = np.hsplit(x3, [2])
upper, lower = np.vsplit(x3, [3])
print(left.shape, right.shape)
print(upper.shape, lower.shape)

a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])

# 要素ごとの積と行列積は意味が違うので、使い分けを明確にする
print(a * b)
print(a @ b)
print(np.dot(a, b))

ベクトル化とブロードキャストを崩さずに使う

ブロードキャストは「次元が違っても計算できる便利機能」ではなく、「形状のルールに従って一括演算できる仕組み」である。keepdims=Truenp.newaxis を使って形を保つと、何を基準に引いたり割ったりしているかが読みやすくなる。
ループで 1 行ずつ処理しても同じことはできるが、ベクトル化を覚えるとコード量と実行速度の両方で有利である。特に標準化や平均との差分は、ブロードキャストで書くと意図が明快になる。

import numpy as np

x = np.arange(5).reshape(1, 5)

# keepdims=True を使うと、集約後も 2 次元の形を保てる
print(x.sum(axis=1, keepdims=True))

matrix = np.arange(12).reshape(3, 4)

# 行平均を保ったまま引くと、各行を中心化できる
row_mean = matrix.mean(axis=1, keepdims=True)
print(matrix - row_mean)

# 列ベクトルと行ベクトルを組み合わせると、外積的な足し算も書ける
col = np.arange(3)[:, np.newaxis]
row = np.arange(4)[np.newaxis, :]
print(col + row)

条件分岐と抽出を列単位で書く

この節では、for 文を増やさずに、条件でデータを切り分ける書き方をまとめます。分析コードでは「ある条件の行だけ見る」「条件ごとにラベルを付ける」が非常に多いので、ここは実務で何度も使う部分です。

比較演算・マスク・ファンシーインデックス

比較演算の結果はブール配列として扱える。all() / any() やファンシーインデックスを組み合わせると、「全部そうか」「どこかにあるか」「特定位置だけ抜くか」を短く書ける。
この段階で大事なのは、条件そのものを変数に置いて読みやすくすることだ。複雑な条件を 1 行へ詰め込みすぎると、後から見たときに誤読しやすい。

import numpy as np

x = np.random.randint(0, 10, (3, 4))

# マスク配列を一度変数へ置くと、条件の意味が追いやすい
mask = (x > 2) & (x < 8)

print(x)
print(mask)
print(np.sum(mask))
print(np.all(x < 10, axis=1))
print(np.any(x == 0))
print(x[mask])

# 行インデックスと列インデックスを同時指定すると、個別位置を抜き出せる
row_idx = np.array([0, 1, 2])
col_idx = np.array([1, 2, 3])
print(x[row_idx, col_idx])

np.where() で条件分岐をベクトル化する

np.where(condition) は条件に合う位置を返し、np.where(condition, True-value, False-value) はラベル付けや簡単な置換を一括で行える。Pandas の列に対してもそのまま使いやすい。
二値の条件分岐を行方向ループで書くより、np.where() にすると「どの条件で、どちらの値を返すか」が明快になる。特徴量の初期ラベル付けで特に便利である。

import numpy as np
import seaborn as sns

iris = sns.load_dataset("iris")

# 条件に合う位置だけを整数インデックスとして取りたい場合
idx = np.where(iris["petal_width"] > 1.3)

# 条件ごとに文字ラベルを付けたい場合
labels = np.where(iris["petal_width"] > 1.3, "wide", "narrow")

print(idx[0][:5])
print(labels[:5])

iris["pw_cat"] = labels
print(iris[["petal_width", "pw_cat"]].head())

np.select() で複数条件をまとめて分類する

np.select() は区間ごとのラベル付けに向く。条件が増えたときでも、入れ子の np.where() や for 文を避けて読みやすく書ける。
条件が 3 つ以上あるときは、np.where() を何段も重ねるより conditionsvalues を分けて書いた方が保守しやすい。特にしきい値分類やスコア区分で重宝する。

import numpy as np
import seaborn as sns

iris = sns.load_dataset("iris")

# 条件配列を先に切り出すと、閾値の意味を見直しやすい
conditions = [
    iris["petal_width"] < 1.0,
    (iris["petal_width"] >= 1.0) & (iris["petal_width"] < 1.8),
    iris["petal_width"] >= 1.8,
]
values = ["narrow", "medium", "wide"]

# default を置くと、どの条件にも当てはまらない値も明示的に扱える
iris["pw_level"] = np.select(conditions, values, default="unknown")
print(iris[["petal_width", "pw_level"]].head())
print(iris["pw_level"].value_counts())

欠損・重複・ラベルをまとめて整える

このまとまりは、実務では最も手数が多く、最も差が出やすい部分です。欠損値処理は「空欄を埋める作業」ではなく、「その空欄をどう解釈するか」を決める作業であり、ここを雑に済ませると、その後の集計・可視化・学習結果まで連鎖的に歪みやすくなります。
まずは「どこに、どれだけ、どんな種類の欠損があるか」を把握し、その後で「落とす」「埋める」「別ラベル化する」「モデル側で扱う」のどれを選ぶかを決めます。逆に、欠損の発生理由を考えずに最初から平均埋めだけをすると、重要な業務情報を消してしまうことがあります。

欠損値の全体像を数えてから手を動かす

欠損値処理では、最初の 5 分で「どこに欠損が多いか」「行単位で偏っているか」「全体の何 % か」を見るだけで、その後の方針がかなり決まる。列ごとの件数しか見ないと、「一部のレコードにだけ欠損が集中している」状況を見逃しやすい。
探索的データ分析の初手としては、列単位・行単位・全体件数・欠損率の 4 つを並べて見るのが使いやすい。逆に、欠損が 1 個見つかった段階で即 dropna() するのは、情報を捨てすぎることが多い。

import numpy as np
import pandas as pd

# 数値列とカテゴリ列が混在した欠損あり DataFrame を作る
df = pd.DataFrame(
    {
        "Date": ["2024-01-01", None, "2024-01-03", None, "2024-01-05"],
        "Color": ["blue", "red", None, "green", None],
        "Quality": [5, np.nan, 8, np.nan, 6],
        "Score": [1.2, np.nan, 2.5, 3.1, np.nan],
        "City": ["Tokyo", None, "Osaka", "Nagoya", None],
    }
)

# 各セルが欠損かどうかを True / False で確認する
print(df.isnull())

# 列ごとの欠損件数
print(df.isnull().sum())

# 行ごとの欠損件数
print(df.isnull().sum(axis=1))

# データセット全体の欠損総数
print(df.isnull().sum().sum())

# 列ごとの欠損率を % で確認する
missing_rate = (df.isnull().mean() * 100).round(1)
print(missing_rate)

# 欠損を含む行だけを見ると、同時欠損のパターンを把握しやすい
print(df[df.isnull().sum(axis=1) > 0])

欠損値を表として可視化し、見落としを減らす

欠損値は件数だけでは把握しきれない。行番号の近いところに連続して抜けているのか、特定の列で帯状に抜けているのかを図で見ると、「入力漏れ」「結合漏れ」「期間途中から取得不能」といった原因を推測しやすくなる。
行数が少ないときは style.highlight_null()、行数が多いときは sns.heatmap(df.isnull()) が便利である。逆に、巨大データで毎回 Styler を使うと重くなるので、レビュー用の少量表示に絞る方がよい。

import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns

df = pd.read_csv("Example_without_index.csv")

# 欠損を 1 つ以上含む行だけに絞ると、レビュー対象を小さくできる
has_null = df.isnull().sum(axis=1) > 0

# null_color は欠損セルを目立たせる色である
df[has_null].style.highlight_null(null_color="#f00")

# heatmap は欠損の偏りを全体で眺めたいときに向く
plt.figure(figsize=(8, 4))
sns.heatmap(
    df.isnull(),
    yticklabels=False,
    cbar=False,
)
plt.title("欠損の分布")
plt.show()

notnull() で「使える行」だけを先に確認する

欠損値処理では、「欠損行を消す」より前に、「この列が埋まっている行だけだと何が見えるか」を確認すると、分析可能な母集団のイメージをつかみやすい。特定列が埋まっているデータだけで十分に仮説検証できることもある。
例えば決済手段や退会理由のように、一部のユーザーにしか存在しない列では、notnull() で利用可能レコードだけを先に観察する方が自然である。逆に、対象母集団そのものを比較したいときは、欠損行だけを都合よく除外すると偏りを作るので注意する。

import seaborn as sns

df = sns.load_dataset("titanic")

# notnull() を使うと、「この列が埋まっている行」だけを明示的に残せる
usable_rows = df[df["embarked"].notnull()].sample(5, random_state=0)
print(usable_rows[["survived", "pclass", "sex", "embarked"]])

dropna() / fillna() を条件付きで使い分ける

dropna() は「この欠損は残しても意味がない」と判断できるときに使う。典型例は、主キー、時刻、目的変数、分析の起点になる列が欠けている場合である。逆に、説明変数の一部が欠けているだけなら、いきなり削除せず補完や別ラベル化も検討したい。
fillna() は「値を補っても意味の破壊が小さい」場合に向く。カテゴリ列なら unknown、フラグ列なら False、売上件数なら 0、連続値なら平均・中央値・グループ別代表値など、列の意味ごとに補完戦略を変えるのが基本である。

import numpy as np
import pandas as pd

# 欠損の意味が列ごとに異なる例を作る
df = pd.DataFrame(
    {
        "Date": ["2024-01-01", None, "2024-01-03", None],
        "Color": ["blue", "red", None, "green"],
        "Quality": [5, np.nan, 8, np.nan],
        "Score": [1.2, np.nan, 2.5, 3.1],
    }
)

# subset は「この列だけは埋まっていてほしい」という必須列を指定する
print(df.dropna(subset=["Date", "Color"]))

# thresh=3 は「非欠損が 3 個以上ある行だけ残す」という意味になる
print(df.dropna(thresh=3))

# 列ごとに補完方法を変えたいときは辞書を使う
filled = df.fillna(
    {
        "Color": "unknown",
        "Quality": 0,
        "Score": df["Score"].mean(),
    }
)
print(filled)

欠損値の補完パターンを増やす

実務では dropna() だけでなく、定数・前方補完・後方補完・平均・中央値・最頻値・列別辞書・グループ別代表値・補間と、補完方法の選択肢が多い。重要なのは「どれが一番きれいか」ではなく、「その列の意味に対して、どの仮説が最も自然か」である。
例えば、在庫数の欠損を 0 と置くのは自然でも、身長や単価を 0 で埋めるのは不自然である。時系列なら前値を引き継ぐ ffill() が自然なことが多いが、イベント発生後に急変する指標では危険なこともある。

import numpy as np
import pandas as pd

# 数値列の補完方法を比較しやすい小さな表を作る
data = pd.DataFrame(
    [[1, np.nan, 3], [4, 5, np.nan], [7, np.nan, 9]],
    columns=["A", "B", "C"],
)

# 定数で埋める
print(data.fillna(0))

# 前方補完と後方補完
print(data.ffill())
print(data.bfill())

# 全体平均で埋める
fill_value = data.stack().mean()
print(data.fillna(fill_value))

# 列ごとに平均・中央値・最頻値を使い分ける例
summary_filled = data.copy()
summary_filled["A"] = summary_filled["A"].fillna(summary_filled["A"].mean())
summary_filled["B"] = summary_filled["B"].fillna(summary_filled["B"].median())
summary_filled["C"] = summary_filled["C"].fillna(summary_filled["C"].mode().iloc[0])
print(summary_filled)

グループごとの代表値で埋める

全体平均で埋めると、カテゴリ差を消してしまうことがある。例えば客単価、身長、購入数のようにグループ差が大きい列では、カテゴリごとの平均・中央値で埋める方が自然である。
「グループ別補完が妥当か」は、補完前後でグループごとの分布差が不自然に縮んでいないかを見ると判断しやすい。グループ列自体の欠損が多い場合は、この方法だけに依存しない方がよい。

import pandas as pd
import seaborn as sns

df = sns.load_dataset("titanic")[["pclass", "fare", "age", "embarked"]].copy()

# pclass ごとの年齢中央値で age を補完する
df["age_filled_by_class"] = df["age"].fillna(
    df.groupby("pclass")["age"].transform("median")
)

# embarked はカテゴリ列なので、最頻値で埋める
df["embarked_filled"] = df["embarked"].fillna(df["embarked"].mode().iloc[0])

print(df[["pclass", "age", "age_filled_by_class", "embarked", "embarked_filled"]].head(10))
print(df[["age", "age_filled_by_class"]].isnull().sum())

時系列では interpolate() も候補に入れる

連続した時間軸を持つデータでは、前値引き継ぎよりも interpolate() の方が自然なことがある。売上、気温、センサー値のように、隣接時点の間を滑らかにつないでよい指標に向く。
一方で、在庫切れや停止状態のように値が飛びやすい業務データでは、線形補間が実態を隠すこともある。補完後は必ず元系列と並べて確認する。

import numpy as np
import pandas as pd

ts = pd.DataFrame(
    {
        "date": pd.date_range("2024-01-01", periods=7, freq="D"),
        "sales": [100, np.nan, np.nan, 130, 128, np.nan, 150],
    }
).set_index("date")

# 時系列の途中欠損を線形補間する
ts["sales_linear"] = ts["sales"].interpolate(
    method="linear",
    limit_direction="both",
)

# ffill と比較すると、どちらが自然かを確認しやすい
ts["sales_ffill"] = ts["sales"].ffill()
print(ts)

Styler.highlight_null() で欠損箇所を表として確認する

欠損行を抽出したあと、style.highlight_null() を使うと確認用の表をそのまま着色できる。レビュー会や notebook 上の目視確認では非常に便利で、どの列が同時に抜けやすいかを一瞬で共有できる。
ただし、Styler は Jinja2 などの依存関係が必要になることがある。環境差で表示できない場合は、無理に Styler にこだわらず、df[df.isnull().sum(axis=1) > 0]sns.heatmap(df.isnull()) へ切り替えればよい。

import pandas as pd

df = pd.read_csv("Example_without_index.csv")
has_null = 0 < df.isnull().sum(axis=1)

df[has_null].style.highlight_null(null_color="#f00")

SimpleImputer で欠損補間を手順として残す

fillna() は軽量だが、学習時と推論時で同じ補完戦略を使いたいなら SimpleImputer の方が扱いやすい。特に機械学習では、学習データで計算した代表値を推論時にも再利用する必要があるため、fittransform を分けられる形が望ましい。
平均・中央値・最頻値・定数埋めを切り替えられるので、列の意味ごとに戦略を変えやすい。逆に、複雑な非線形関係まで補いたいなら、SimpleImputer だけで十分とは限らない。

import pandas as pd
from sklearn.impute import SimpleImputer

df = pd.read_csv("Example_without_index.csv", usecols=["Width", "Height"])

mean_imputer = SimpleImputer(strategy="mean")
median_imputer = SimpleImputer(strategy="median")

df["Width_Imputed"] = mean_imputer.fit_transform(df[["Width"]])
df["Height_Imputed"] = median_imputer.fit_transform(df[["Height"]])

print(df[df["Width"].isnull() | df["Height"].isnull()])

train = pd.DataFrame({"Score": [10.0, 12.0, None, 15.0]})
test = pd.DataFrame({"Score": [None, 14.0]})

score_imputer = SimpleImputer(strategy="median")
score_imputer.fit(train[["Score"]])
train["Score_filled"] = score_imputer.transform(train[["Score"]])
test["Score_filled"] = score_imputer.transform(test[["Score"]])

print(df.head())
print(train)
print(test)

補完後は「埋まったか」ではなく「壊していないか」を確認する

欠損値処理の成否は、NaN が消えたかどうかだけでは判断できない。本当に見るべきなのは、補完前後で平均・中央値・分位点・カテゴリ比率・グループ差が不自然に変わっていないかである。
探索段階では、補完前後の要約統計量を横に並べるだけでも十分役立つ。モデル前処理ではさらに、補完後に目的変数との関係や分布図も見直すと安心である。

import numpy as np
import pandas as pd

df = pd.DataFrame(
    {
        "Score": [1.2, np.nan, 2.5, 3.1, np.nan, 2.9],
        "Group": ["A", "A", "A", "B", "B", "B"],
    }
)

filled = df.copy()
filled["Score"] = filled["Score"].fillna(
    filled.groupby("Group")["Score"].transform("median")
)

before_stats = df["Score"].describe()
after_stats = filled["Score"].describe()
comparison = pd.concat([before_stats, after_stats], axis=1)
comparison.columns = ["before", "after"]
print(comparison)

print(filled.isnull().sum())

duplicated() / drop_duplicates() で重複を処理する

欠損値と同様に、重複も「見つけたら消す」ではなく、「なぜ重複したか」を考えるべき対象である。同一レコードの二重登録なのか、時点違いの再観測なのか、結合ミスなのかで対処が変わる。
探索段階では、まず duplicated() で候補を可視化し、その後で subset= を使って「どの列の組み合わせを同一とみなすか」を決めるのが安全である。逆に、イベントログのように同じ値が繰り返されるのが自然なデータでは、安易に重複削除しない。

import pandas as pd

df = pd.DataFrame(
    {
        "A": [1, 1, 2, 2],
        "B": ["x", "x", "y", "z"],
        "Date": ["2024-01-01", "2024-01-01", "2024-01-02", "2024-01-03"],
    }
)

print(df.duplicated())
print(df.drop_duplicates())
print(df.drop_duplicates(subset=["A", "B"]))

ラベル整列つき演算と fill_value

Pandas の演算は位置ではなくラベルで揃う。したがって、キーがずれたまま加算・除算すると NaN が自然に発生する。これはバグではなく、「その組み合わせに値が存在しない」ことを表す重要な信号である。
売上や在庫のように「存在しないなら 0 とみなしてよい」場面では、add(..., fill_value=0)sub(..., fill_value=0) のようなメソッド版が役立つ。逆に、医療値やセンサー値のように「測定されていないこと」自体が意味を持つ場面では、安易に 0 埋めしない方が安全である。

import numpy as np
import pandas as pd

A = pd.Series([2, 4, 6], index=[0, 1, 2])
B = pd.Series([1, 3, 5], index=[1, 2, 3])

print(A + B)
print(A.add(B, fill_value=0))

x = pd.DataFrame(np.arange(1, 10).reshape(3, 3), columns=list("ABC"))
print(x.div(x["A"], axis=0))
print(x.div(x.iloc[0], axis=1))

apply() による列方向(横)・行方向(縦)の処理

apply() は列ごと・行ごとに同じ処理をまとめて適用できる。欠損値処理でも、「列ごとに別の補完値を計算する」「行ごとに欠損率を出す」といった前処理を短く書きたいときに便利である。
ただし、単純な四則演算や fillna() だけで済む処理まで何でも apply() で書くと、遅くなりやすく、コードの意図も読み取りにくくなる。ベクトル化で書ける部分と、apply() が必要な部分を分けて考えるのが実務的である。

import numpy as np
import pandas as pd

df = pd.DataFrame(
    np.random.randint(1, 10, size=(4, 3)),
    columns=["A", "B", "C"],
)

print(df.apply(np.cumsum))
print(df.apply(np.cumsum, axis=1))
print(df.apply(lambda col: col.max() - col.min()))

set_index() / reset_index() / rename() / add_prefix() でラベルを整える

ラベル整理は「見た目の整形」ではなく、後続の欠損確認・集計・結合を安全にするための準備である。列名が曖昧なままだと、補完した列なのか原列なのかが分からなくなり、分析の再現性が落ちやすい。
補完前後で列名を分ける、接頭辞を付ける、インデックスを振り直す、といった小さな整理が、あとで差分確認やレビューをしやすくする。逆に、意味のない接頭辞を増やしすぎると読みにくくなるので、役割が変わる列だけに絞るとよい。

import pandas as pd

df = pd.read_csv("Example_without_index.csv").head(3)
print(df.index)

df2 = df.set_index("Date")
df3 = df2.rename(columns={"Price": "UnitPrice"}).add_prefix("#").reset_index()

print(pd.RangeIndex(1, 20, 4).values)
print(df3.head())

Category型で順序とコードを持たせる

pd.Categorical() を使うと表示順や比較順を固定できる。欠損補完後に unknown を追加したカテゴリ列でも、可視化やクロス集計の順番を崩さずに扱える。
探索的分析では、「欠損を埋めた結果、カテゴリの並びまで崩れた」という状態を避けたい。逆に、順序を持たない純粋なラベル列まで無理に順序化すると、意味のない比較を誘発することがある。

import pandas as pd
import seaborn as sns

df = sns.load_dataset("iris")

df["species_ordered"] = pd.Categorical(
    df["species"],
    ordered=True,
    categories=["versicolor", "virginica", "setosa"],
)

print(df["species_ordered"].cat.categories)
print(df["species_ordered"].cat.codes[:5].tolist())

データ集計

この章では、まず全体像をざっと読み、その後にグループ単位の集約へ進み、最後に MultiIndex や集計表の形を自在に変える流れで整理します。

まず全体像をつかむ

sort / argsort / argmax / 統計量で配列の輪郭をつかむ

sort()argsort() は順序確認、argmax() / argmin() は位置確認、median() / mean() / std() は代表値の把握に向く。

import numpy as np

x = np.random.randint(20, 50, size=10)
print(np.sort(x))
print(np.argsort(x))
print(np.argmax(x), np.argmin(x))

y = np.random.normal(0, 1, size=10000)
print(np.median(y))
print(np.mean(y))
print(np.std(y))

value_counts() / mode() / nlargest() / nsmallest() で頻度と極値を読む

value_counts() は頻度確認、mode() は最頻値、nlargest() / nsmallest() は極端なレコードの確認に向く。探索の初期段階で異常値候補を拾いやすい。
小売では購入個数の偏り、EC ではレビュー件数や注文数の極端値を最初に見る場面が多く、どのカテゴリが支配的か、どのレコードが外れ気味かを短時間で把握できる。

import numpy as np
import pandas as pd

df = pd.read_csv("Example_without_index.csv")

print(df["Quantity"].value_counts().head())
print(np.array(df["Quantity"].mode()))
print(df["Quantity"].nlargest(5))
print(df.loc[df["Quantity"].nsmallest(5).index, ["Quantity", "Price"]])

nunique() と describe().T で一覧性を上げる

nunique() はユニーク値の数だけを素早く確認したいときに向く。describe().T は統計量表を転置し、列ごとの比較を縦方向に読みやすくする。
例えば店舗 ID、カテゴリ数、会員ランクの種類数をざっと点検するとき、まず nunique() で粒度を確認し、その後 describe().T で価格や数量の分布を縦に並べると、調査の優先順位を付けやすい。

import pandas as pd

df = pd.read_csv("Example_without_index.csv")

print(df["Quantity"].nunique())
print(df.describe().T.head())

グループ単位の集計を広げる

この節では、「列全体を見る」段階から一歩進んで、「誰ごと・商品ごと・月ごと」のような単位で差を見る力を付けます。集計の本質は平均を出すことではなく、どの単位で比較すると仮説が見えやすいかを決めることです。

groupby() の aggregate / filter / transform / apply を使い分ける

groupby() は平均や合計だけでなく、複数統計量の集約、グループ除外、グループ内正規化まで一連で書ける。
小売・EC では「店舗ごとの平均客単価」「カテゴリごとの売上構成比」「一定件数未満の店舗を除外した比較」が頻出するので、この 4 つを分けて考えられると実務でそのまま役立つ。

import pandas as pd

df = pd.DataFrame(
    {
        "key": ["A", "A", "B", "B", "C"],
        "data1": [1, 2, 3, 4, 5],
        "data2": [10, 20, 30, 40, 50],
    }
)

print(df.groupby("key").aggregate(["min", "median", "max"]))

# filter は「条件を満たすグループだけ残す」ときに使う
print(df.groupby("key").filter(lambda g: g["data2"].mean() >= 25))

# transform は行数を保ったまま、グループ基準の値へ変換できる
print(df.groupby("key")["data2"].transform(lambda s: s - s.mean()))

def normalize_by_group(group):
    group = group.copy()
    group["rate"] = group["data2"] / group["data2"].sum()
    return group

print(df.groupby("key", group_keys=False).apply(normalize_by_group))

groupby().groups と get_group() でグループの中身を直接確認する

groupby() は集計だけでなく、グループ名と所属インデックスの対応確認や、特定グループのレコード抽出にも使える。前処理の確認で便利な使い方である。
例えば「特定ブランドだけ欠損が多い」「特定配送方法だけ返品率が高い」と気付いたとき、まず該当グループの生レコードを直接見ると原因を追いやすい。

import seaborn as sns

df = sns.load_dataset("penguins").dropna()

grouped = df.groupby("species")
print(grouped.groups.keys())
print(grouped.groups["Adelie"][:5])
print(grouped.get_group("Gentoo").head(3))

unstack() と高度な pivot_table() で表形式に戻す

複数キーの集計は unstack() で表形式へ戻せる。pivot_table()margins=True や複数集約関数と相性が良い。
店舗 x カテゴリ、チャネル x 月、会員ランク x 商品群のような二軸比較では、行列形式へ戻した方が差が一目で分かる。特に会議用の一覧表では pivot_table() の形がそのまま使いやすい。

import pandas as pd

sales = pd.DataFrame(
    {
        "sex": ["female", "female", "male", "male"],
        "class": ["First", "Third", "First", "Third"],
        "survived": [1, 0, 0, 0],
        "fare": [100, 20, 80, 15],
    }
)

table1 = sales.groupby(["sex", "class"])["survived"].mean().unstack()
table2 = sales.pivot_table(
    index="sex",                         # 行方向(縦)に並べるカテゴリ
    columns="class",                    # 列方向(横)に並べるカテゴリ
    values=["survived", "fare"],       # 集計対象の値列
    aggfunc={"survived": "mean", "fare": "mean"},  # 列ごとの集計方法
    margins=True,                        # 全体行・全体列を追加する
)

print(table1)
print(table2)

crosstab() / groupby() / pivot_table() の違いを整理する

crosstab() はカテゴリ同士の件数や構成比を表にするときに向く。groupby() は「分ける -> 集計する / 変換する / 絞る」という汎用の処理基盤であり、最も自由度が高い。pivot_table()index / columns / values / aggfunc を明示し、表計算ソフトのピボットテーブルのように集計表を作りたいときに使う。
実務では、件数確認なら crosstab()、処理込みの集計なら groupby()、報告しやすい表にしたいなら pivot_table() と覚えると判断が速い。小売の来店件数、EC の注文件数、施策別の平均単価比較で特に使い分けやすい。

import pandas as pd

sales = pd.DataFrame(
    {
        "sex": ["female", "female", "male", "male", "male"],
        "class": ["First", "Third", "First", "Third", "Third"],
        "survived": [1, 0, 0, 0, 1],
        "fare": [100, 20, 80, 15, 18],
    }
)

print(pd.crosstab(sales["sex"], sales["class"], margins=True))
print(sales.groupby(["sex", "class"])["fare"].mean())
print(sales.pivot_table(index="sex", columns="class", values="fare", aggfunc="mean", margins=True))

MultiIndex の xs() で断面を抜き出す

複数キーで groupby() した結果は MultiIndex になることが多い。xs() を使うと、特定レベルだけで素早く断面抽出できる。
例えば 店舗 x カテゴリ で集計したあとに「飲料カテゴリだけを全店舗比較したい」といった場面では、xs() があると再集計せずに必要部分だけ抜き出せる。

import pandas as pd

df = pd.read_csv("Example_without_index.csv")
summary = df.groupby(["Color", "Shape"]).describe()

print(summary.xs("circle", level="Shape").head())
print(summary.index.get_level_values("Shape")[:5])

データ可視化

この章では、素早い一次確認から、Matplotlib の細かな調整、Seaborn の多変量可視化、最後の見た目調整までを一連の流れとして並べます。

すばやく全体像を描く

この節では、分析の初手で使う「速い可視化」をまとめます。ここでの目的はレポート映えではなく、異常値、偏り、欠損補完後の歪み、比較対象の候補を短時間で見つけることです。

Pandas の plot メソッドで集計結果を素早く可視化する

plot(kind='bar') だけでなく、barhhistkdescattersubplots まで一通り使えるようになると、集計結果の一次確認がかなり速くなる。
小売ではカテゴリ別売上の棒グラフ、EC では価格帯分布のヒストグラム、指標推移の小分割折れ線をまずざっと出すことが多い。凝った装飾より、異常値や偏りをすばやく見つけられることが重要である。

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

df_bar = pd.DataFrame(np.random.rand(6, 4), columns=list("ABCD"))
# stacked=True で構成比を 1 本の棒の中で比較しやすくする
df_bar.plot.bar(stacked=True, figsize=(6, 4))
plt.show()

# 横向きにするとカテゴリ名が長いときでも読ませやすい
df_bar.plot.barh(stacked=True, figsize=(6, 4))
plt.show()

df_hist = pd.DataFrame(
    {
        "A": np.random.randn(1000) - 3,
        "B": np.random.randn(1000),
        "C": np.random.randn(1000) + 3,
    }
)
# bins は粒度、alpha は重なった棒の見やすさを調整する
df_hist.plot.hist(bins=40, alpha=0.6)
plt.show()

# cumulative=True で累積比率の立ち上がりを確認する
df_hist["A"].plot.hist(cumulative=True)
plt.show()

# KDE は分布の山の位置だけを滑らかに比較したいときに便利
df_hist["A"].plot(kind="kde")
plt.show()

housing = pd.DataFrame(
    {
        "median_income": np.random.rand(100),
        "median_house_value": np.random.rand(100) * 500000,
    }
)
# alpha を下げると、点が密集している領域を読み取りやすい
housing.plot(kind="scatter", x="median_income", y="median_house_value", alpha=0.6)
plt.show()

df_line = pd.DataFrame(
    np.random.randn(100, 4).cumsum(axis=0),
    columns=list("ABCD"),
)
# subplots=True で列ごとに別軸へ分け、指標ごとの差を混同しにくくする
df_line.plot(subplots=True, figsize=(6, 16))
plt.tight_layout()
plt.show()

# layout で並び方を制御すると、複数指標を一覧しやすい
# sharex=False で x 軸を別々に持たせると、指標ごとの動きを見やすくできる
df_line.plot(subplots=True, layout=(2, 2), figsize=(10, 6), sharex=False)
plt.tight_layout()
plt.show()

Seaborn のスタイルをまとめて切り替える

sns.set() を使うと、Matplotlib の同じ折れ線グラフでも見た目を一括で整えられる。探索段階で体裁を早く整えたいときに便利。
短時間で何枚も図を出す探索段階では、毎回色や背景を手で整えるより、スタイルを先にそろえて解釈に集中した方が効率がよい。

import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns

x = np.linspace(0, 10, 500)
y = np.cumsum(np.random.randn(500, 6), axis=0)

sns.set()
plt.figure(figsize=(10, 6))
plt.plot(x, y)
plt.legend("ABCDEF", ncol=2, loc="upper left")
plt.show()

スタイル切り替えと画像保存

plt.style.context() なら一時的に描画スタイルを切り替えられる。savefig() を組み合わせると同じ設定で画像を保存できる。
社内共有用の図だけ別スタイルで保存したい、レポート用だけ白背景にしたい、といった場面では、この書き方にしておくと周囲の図へ影響を広げずに済む。

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 100)
y = np.exp(x / 10)

with plt.style.context("seaborn-white"):
    plt.plot(x, y)
    plt.title("スタイル切り替えの例")
    plt.savefig("style_context_example.png")
    plt.show()

Matplotlib の基本調整を固める

Matplotlib の細かな調整は、図をきれいにするためだけではなく、誤読を減らすために行います。同じデータでも軸範囲や凡例位置が悪いと結論が逆に見えることがあるので、ここは実務でも軽視しない方がよいです。

軸・目盛り・対数スケールの調整

xlim() / ylim() / axis() / xscale("log") / tick_params() を使うと、同じデータでも読みやすさが大きく変わる。
売上規模が店舗ごとに大きく異なるとき、対数軸にするだけで中小店舗の動きも読めるようになる。目盛りの大きさ調整も、会議資料では意外と重要である。

import matplotlib.pyplot as plt
import numpy as np

x = np.logspace(0, 5, 100)
y = np.log(x)

plt.plot(x, y)
# 桁差が大きい横軸を圧縮し、広い範囲を 1 枚で読めるようにする
plt.xscale("log")
plt.xlim(1, 1e5)
plt.ylim(0, 12)
# tick_params でラベルサイズを上げると投影資料でも読みやすい
plt.tick_params(axis="both", labelsize=12)
plt.show()

折れ線グラフの線種・線幅・マーカーを細かく調整する

plot()legend() の基本に加えて、linestyle / linewidth / marker / markersize まで使い分けると、複数系列を並べたときの読みやすさが大きく変わる。
例えば通常価格、値引き後価格、目標ラインを 1 枚で比較するとき、線種やマーカーを分けておくと白黒印刷でも見分けやすい。

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 11)

plt.plot(x, x, "g-", label="solid")
plt.plot(x, x + 2, linestyle="--", linewidth=3, label="dashed")
plt.plot(x, x + 4, marker="o", markersize=8, label="marker")
plt.plot(x, x + 6, "rs:", label="short style")
plt.legend(loc="upper left", frameon=True, fontsize=12)
plt.show()

text() と annotate() による注釈

text() は簡単な文字追加、annotate() は矢印付きの説明に向いている。重要な点を明示したいときに便利。
セール開始日、在庫切れ、広告配信開始のようなイベントを図上へ直接書き込めると、数字を知らない相手にも変化の理由を伝えやすい。

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 2 * np.pi, 100)
y = np.sin(x)

plt.plot(x, y, "b-")
plt.text(3.5, 0.5, "y = sin(x)", fontsize=12)
plt.annotate(
    "local min",
    xy=(1.5 * np.pi, -1),
    xytext=(4.5, 0),
    # arrowprops で矢印の見た目をまとめて指定する
    arrowprops={"facecolor": "black", "shrink": 0.1},
)
plt.show()

errorbar() による誤差表示

実測データや推定値は、中心値だけでなく誤差幅も一緒に描くと解釈しやすい。
店舗別平均売上や広告効果の推定では、平均だけ示すと差が大きく見えすぎることがある。誤差幅まで併記すると、ばらつき込みで判断しやすい。

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 20)
dy = 0.5
y = np.sin(x) + dy * np.random.randn(20)

# yerr が誤差幅、capsize が端の横棒の大きさを表す
plt.errorbar(x, y, yerr=dy, fmt="+b", capsize=4)
plt.show()

オブジェクト指向スタイルで Axes を管理する

FigureAxes を変数として持つと、主図と挿入図の両方を明示的に制御しやすい。
売上推移の主図に、販促期間だけの拡大図を差し込むような場面では、Axes を明示しておく方が後から調整しやすい。

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 5, 10)
y = x**2

fig = plt.figure(figsize=(8, 4))
ax1 = fig.add_axes([0.1, 0.1, 0.8, 0.8])
ax2 = fig.add_axes([0.2, 0.5, 0.3, 0.3])

ax1.plot(x, y, "r")
ax1.set_title("主図")

ax2.plot(y, x, "g")
ax2.set_title("挿入図")
plt.show()

GridSpec で不規則レイアウトを作る

GridSpec を使うと、一部のグラフだけ横長にするような不規則レイアウトを組める。ダッシュボード風の図に向く。
KPI サマリーでは、主役の売上推移だけ大きく、補助の在庫や客数は小さく並べたいことが多い。GridSpec はそうした情報の強弱付けに便利である。

import matplotlib.pyplot as plt
import numpy as np

def f(x):
    return np.exp(-x) * np.cos(2 * np.pi * x)

x = np.arange(0.0, 3.0, 0.01)
grid = plt.GridSpec(2, 3, wspace=0.4, hspace=0.3)

# 0 行 0 列のセルに 1 枚置く
plt.subplot(grid[0, 0])
plt.plot(x, f(x))

# 0 行目の右 2 列を横長で使う
plt.subplot(grid[0, 1:])
plt.plot(x, f(x), "r--", lw=2)

# 1 行目全体を 1 枚で使う
plt.subplot(grid[1, :])
plt.plot(x, f(x), "g-.", lw=3)
plt.show()

3D プロットで曲面を描く

projection="3d" を使うと、二変数関数の曲面も NumPy 配列からそのまま描ける。
実務で 3D を多用する必要はないが、価格と広告費の組み合わせに対する指標の変化を直感的に見せたいときには有効である。

import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits import mplot3d

def f(x, y):
    return np.sin(np.sqrt(x**2 + y**2))

x = np.linspace(-6, 6, 30)
y = np.linspace(-6, 6, 30)
X, Y = np.meshgrid(x, y)
Z = f(X, Y)

ax = plt.axes(projection="3d")
# cmap で高さの違いを色でも読めるようにする
ax.plot_surface(X, Y, Z, cmap="viridis")
plt.show()

代表的な図を使い分ける

散布図で色・大きさ・透明度を同時に使う

散布図は座標だけでなく、salpha、さらに colorbar() を組み合わせることで、1 枚に複数の情報を載せられる。
店舗位置 x 売上、商品価格 x 販売数、広告費 x CV 数のように、2 変数以上を同時に読みたい場面で特に有効である。

import matplotlib.pyplot as plt
import numpy as np

x, y, colors, size = (np.random.rand(100) for _ in range(4))

# c は色、s は点サイズ、alpha は重なりの見やすさを制御する
plt.scatter(x, y, c=colors, s=1000 * size, cmap="viridis", alpha=0.3)
plt.colorbar()
plt.show()

柱形図は積み上げ・並列・横向きを使い分ける

棒グラフは 1 種類だけではなく、積み上げ、並列、横向き、さらに sns.barplot() まで押さえておくと、比較したい内容に応じて素早く描き分けられる。
売上構成比なら積み上げ、店舗比較なら並列、カテゴリ名が長いなら横棒、平均値比較なら sns.barplot() と考えると選びやすい。

import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns

plt.figure(figsize=(12, 4))

x = np.arange(5)
y1 = np.array([20, 24, 18, 26, 22])
y2 = np.array([15, 18, 20, 17, 19])

plt.subplot(131)
# bottom=y1 にすると B を A の上へ積み上げられる
plt.bar(x, y1, width=0.5, label="A")
plt.bar(x, y2, width=0.5, bottom=y1, label="B")
plt.legend()

plt.subplot(132)
# x + 0.3 とずらして並列比較できるようにする
plt.bar(x, y1, width=0.3, label="A")
plt.bar(x + 0.3, y2, width=0.3, label="B")
plt.legend()

plt.subplot(133)
sns.barplot(x=y1[::-1], y=["G5", "G4", "G3", "G2", "G1"], linewidth=3)

plt.tight_layout()
plt.show()

統計量を意識したヒストグラム

density=True は確率密度、cumulative=True は累積分布、histtype="step" は重ね描き向けの表示になる。
会員単価や配送日数の分布を見るとき、件数だけでなく比率や累積の形まで見ると、閾値設定や異常検知の判断がしやすい。

import matplotlib.pyplot as plt
import numpy as np

x = 100 + 15 * np.random.randn(10000)

plt.figure(figsize=(10, 4))

plt.subplot(131)
plt.hist(x, bins=30, density=True, color="r")
plt.title("density")

plt.subplot(132)
plt.hist(x, bins=30, density=True, cumulative=True, color="b")
plt.title("cumulative")

plt.subplot(133)
plt.hist(x, bins=30, density=True, histtype="step", color="g")
plt.title("step")

plt.tight_layout()
plt.show()

Seaborn で分布・カテゴリ・関係を見る

Seaborn の強みは、huerowcol などを足すだけで比較軸を自然に増やせる点にあります。反面、1 枚に情報を載せすぎると読みにくくなるので、「まず全体」「次に切り分け」の順で使うと実務では扱いやすいです。

Seaborn の pairplot() で多変量を一気に確認する

pairplot() は複数特徴量の散布図行列と分布をまとめて確認できる。探索的データ分析の入口として強い。
商品属性、顧客属性、注文指標のどの組み合わせが分かれそうかを最初にざっと見る用途に向いている。特徴量候補を絞る前段として便利である。

import seaborn as sns

iris = sns.load_dataset("iris")
# hue を入れるとカテゴリ別の分離具合を一気に確認できる
sns.pairplot(data=iris, hue="species")

histplot() / kdeplot() で分布の形を比較する

histplot()multiple / stat / kde=True によって見せ方を変えられる。kdeplot() は分布の形を滑らかに比較したいときに便利である。
小売の客単価、EC の配送日数、会員ランク別の購入金額など、カテゴリごとの分布差を見たいときに最も使いやすい組み合わせである。

import matplotlib.pyplot as plt
import seaborn as sns

df = sns.load_dataset("penguins").dropna()
sns.set_theme(context="talk", style="darkgrid", font="MS Gothic")

plt.figure(figsize=(8, 5))
sns.histplot(
    data=df,                 # 可視化対象の DataFrame
    x="body_mass_g",        # 横軸に置く数値列
    hue="species",          # 色で分けるカテゴリ列
    multiple="dodge",       # 棒を横に並べて比較する
    shrink=0.8,              # 棒幅を少し細くして重なりを減らす
    stat="probability",     # 件数ではなく比率で比較する
    kde=True,                # 分布のなめらかな曲線も重ねる
)
plt.show()

plt.figure(figsize=(8, 5))
sns.kdeplot(data=df, x="body_mass_g", hue="species", multiple="fill", palette="viridis")
plt.show()

catplot() / countplot() でカテゴリ全体を俯瞰する

catplot()row / col / hue を組み合わせたグリッド表示が得意で、countplot() はカテゴリ件数の比較に向く。kind="strip" を使えば散布の重なりも把握しやすい。
店舗区分、配送方法、会員ランクのようなカテゴリ列を俯瞰するときに有効で、どの切り口でばらつきが大きいかを先に発見しやすい。

import matplotlib.pyplot as plt
import seaborn as sns

df = sns.load_dataset("titanic").dropna(subset=["age", "embarked"])

sns.catplot(
    data=df,
    x="embarked",           # カテゴリ軸
    y="age",                # 数値軸
    hue="sex",              # 色で比較したいカテゴリ
    col="pclass",           # 客室等級ごとに列方向(横)へ分割する
    kind="strip",           # 個々の点を見せて分布の広がりを確認する
    height=4,
    aspect=1.2,
)
plt.show()

plt.figure(figsize=(6, 4))
# countplot は件数比較に特化しており、カテゴリ比率の偏り確認に向く
sns.countplot(data=df, x="survived", hue="embarked", palette="bone")
plt.xticks([0, 1], ["No", "Yes"])
plt.show()

boxplot() / violinplot() / swarmplot() を使い分ける

boxplot() は四分位と外れ値、violinplot() は分布形状、swarmplot() は各点の密集度まで確認したいときに向く。カテゴリごとの数値分布を見るときの定番である。
例えば店舗タイプ別の客単価、配送方法別の配送日数、会員区分別の購入回数を比較するとき、どこまで細かく見たいかで図を選び分けるとよい。

import matplotlib.pyplot as plt
import seaborn as sns

df = sns.load_dataset("titanic").dropna(subset=["age", "fare"])

fig, axes = plt.subplots(1, 3, figsize=(16, 5), tight_layout=True)

# sym="" で外れ値マーカーを消すと、箱そのものを読みやすくできる
sns.boxplot(data=df, x="pclass", y="fare", hue="who", palette="Set3", sym="", ax=axes[0])
# split=True は 2 群を左右で 1 本のバイオリンに載せて比較する指定
sns.violinplot(data=df, x="sex", y="age", hue="survived", split=True, palette="spring", ax=axes[1])
# dodge=True で hue ごとの点を横へずらし、重なりを減らす
sns.swarmplot(data=df, x="sex", y="age", hue="survived", dodge=True, size=3, palette="prism", ax=axes[2])

axes[0].set_title("boxplot")
axes[1].set_title("violinplot")
axes[2].set_title("swarmplot")
plt.show()

jointplot() / scatterplot() / relplot() で関係を分解する

jointplot() は周辺分布付き、scatterplot() は 1 枚の軸で柔軟に調整、relplot() は行列方向への分割表示が得意である。数値変数同士の関係を見るときに役立つ。
価格と数量、広告費と CV、商品サイズと返品率のように、2 変数の関係を軸に追加情報を重ねたいときの基本セットである。

import matplotlib.pyplot as plt
import seaborn as sns

df = sns.load_dataset("penguins").dropna()

plt.figure(figsize=(8, 6))
sns.scatterplot(
    data=df,
    x="bill_length_mm",
    y="bill_depth_mm",
    hue="species",          # 色で種別を区別する
    style="sex",            # マーカー形状で性別を区別する
    size="body_mass_g",     # 点サイズで体重も同時に表す
    sizes=(20, 200),         # 最小サイズと最大サイズ
    alpha=0.7,               # 点の重なりを見やすくする透明度
)
plt.legend(bbox_to_anchor=(1, 1))
plt.show()

sns.jointplot(
    data=df,
    x="body_mass_g",
    y="bill_length_mm",
    hue="species",
    height=6,
    kind="scatter",
)
plt.show()

sns.relplot(
    data=df,
    x="bill_length_mm",
    y="bill_depth_mm",
    hue="sex",              # 色で性別差を見る
    col="island",           # 島ごとに列方向(横)へ分ける
    row="species",          # 種ごとに行方向(縦)へ分ける
    size="body_mass_g",     # 点サイズで体重を重ねる
    sizes=(20, 200),
    height=3,
    aspect=1.6,
)
plt.show()

regplot() / lmplot() で回帰直線を重ねる

regplot() は単一の軸に回帰直線を重ねたいとき、lmplot() はクラスごとに分けて回帰傾向を比較したいときに向く。散布図だけでは読みにくい傾向を補える。
値引き率と販売数、広告費と売上のような関係をざっと見たいとき、まず回帰直線を重ねて方向感を確かめると議論が速い。

import matplotlib.pyplot as plt
import seaborn as sns

df = sns.load_dataset("penguins").dropna()
adelie = df[df["species"] == "Adelie"]

plt.figure(figsize=(6, 4))
# 単一グループ内の傾向だけをまず見ると、全体平均との差に惑わされにくい
sns.regplot(data=adelie, x="bill_length_mm", y="bill_depth_mm")
plt.show()

sns.lmplot(
    data=df,
    x="bill_length_mm",
    y="bill_depth_mm",
    col="species",
    row="island",
    truncate=False,
    height=3,
    aspect=1.4,
)
plt.show()

heatmap() / clustermap() で相関行列を読む

heatmap() は相関の強さをそのまま読み取りやすく、clustermap() は似た変数同士を近くに並べて構造を見つけやすい。探索段階での変数整理に向く。
商品属性や顧客特徴量が多いとき、まず似た列をまとめて見つけると、冗長な特徴量の整理や説明の順番を決めやすい。

import matplotlib.pyplot as plt
import seaborn as sns

df = sns.load_dataset("iris")
corr = df.corr(numeric_only=True)

plt.figure(figsize=(6, 5))
# annot=True で値を書き込み、fmt で小数桁数をそろえる
sns.heatmap(corr, annot=True, fmt=".3f", cmap="Spectral", vmin=-1, square=True, linewidths=1)
plt.show()

# clustermap は似た列同士を自動で近くへ並べ替える
sns.clustermap(corr, annot=True, fmt=".3f", cmap="Spectral", vmin=-1, linewidths=1, figsize=(6, 6))
plt.show()

PairGrid() / FacetGrid() で高自由度のグリッドを組む

PairGrid() は対角・上三角・下三角で別の描画関数を割り当てられる。FacetGrid() はカテゴリの組み合わせごとに軸を分割し、同じ描画を反復するときに強い。
分析の終盤で「この切り口とあの切り口を同時に見たい」が増えてきたときに有効で、店舗群ごと・カテゴリ群ごとの比較を細かく制御しやすい。

import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns

df = sns.load_dataset("iris")

g = sns.PairGrid(data=df, hue="species", vars=df.select_dtypes(include="number").columns, height=3)

# 対角には分布、上三角には密度、下三角には散布図を置き、同じ変数群を別視点で確認する
g.map_diag(sns.histplot, multiple="stack")
g.map_upper(sns.kdeplot)
g.map_lower(sns.scatterplot, alpha=0.6)
g.add_legend()
plt.show()

df["pw_cat"] = pd.cut(df["petal_width"], bins=[-float("inf"), 1.0, 2.0, float("inf")], labels=["narrow", "medium", "wide"])
df["pl_cat"] = pd.cut(df["petal_length"], bins=3, labels=["short", "middle", "long"])

g = sns.FacetGrid(data=df, row="pw_cat", col="pl_cat", hue="species", height=3.5)
g.map_dataframe(sns.scatterplot, x="sepal_length", y="sepal_width")
g.add_legend()
plt.show()

複数要素を載せて仕上げる

この節は、分析結果を他人へ見せる最終段階を意識しています。単一の図では伝わりにくいときに、補助線、注釈、複数軸、保存設定を組み合わせて、読み手の負荷を減らすのが目的です。

円グラフと複数 Axes の表示調整で仕上げる

円グラフ、補助線、注釈、凡例位置、背景色、保存設定をまとめて使えると、同じデータでも伝わりやすい図へ仕上げやすくなる。最後の見た目調整も重要な技術である。
最終共有用の図では、単に描くだけでなく「平均線はどこか」「注目点はどこか」「画像として再利用できるか」まで整えておくと、読み手の解釈がぶれにくい。

import matplotlib.pyplot as plt
import seaborn as sns

df = sns.load_dataset("iris")
counts = df["species"].value_counts()

fig, axes = plt.subplots(1, 2, figsize=(12, 5), facecolor="#f3f1ea", tight_layout=True)
fig.subplots_adjust(wspace=0.35)

axes[0].pie(
    counts,
    labels=counts.index,       # ラベル名を表示する
    autopct="%.1f%%",         # 割合を小数 1 桁で描く
    startangle=90,             # 90 度から描き始める
    counterclock=False,        # 時計回りに並べる
    explode=[0, 0, 0.08],      # 最後のカテゴリだけ少し外へ出す
)
axes[0].set_title("species ratio")

sns.scatterplot(data=df, x="petal_width", y="petal_length", hue="species", ax=axes[1])
# 平均線を入れると、各点が全体平均に対して高いか低いかをすぐ判断できる
axes[1].axhline(df["petal_length"].mean(), color="red", linestyle="--")
axes[1].axvline(df["petal_width"].mean(), color="blue", linestyle=":")
axes[1].annotate(
    "mean area",
    xy=(1.3, 4.2),
    xytext=(0.2, 6.2),
    arrowprops={"arrowstyle": "->", "color": "#444"},
)
axes[1].legend(bbox_to_anchor=(1, 1))
# 背景色を変えると、主図だけ少し強調できる
axes[1].set_facecolor("#fff8e8")

# 保存時の dpi は見た目の細かさに影響する
plt.savefig("advanced_plot_examples.png", dpi=120)
plt.show()

差分ヒストグラムと地理散布図で変化量まで確認する

単純な列ごとの可視化だけでなく、diff() をかけた変化量のヒストグラムや、緯度・経度・人口・価格を同時に載せる地理散布図まで使えると、探索的分析の幅が大きく広がる。
小売・EC でも、前日差分の分布で急変を探す、店舗位置と売上水準を同時に地図的な散布図で読む、といった発想はそのまま応用できる。

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns

df = pd.DataFrame(
    np.random.randn(1000, 4).cumsum(axis=0),
    columns=list("ABCD"),
)
# diff() で 1 期ごとの変化量へ変換し、急変が多い列を見つける
df.diff().hist(bins=50, color="r")
plt.tight_layout()
plt.show()

housing = pd.read_csv("housing.csv")
with sns.axes_style("white"):
    housing.plot(
        kind="scatter",
        x="longitude",                    # 横軸に経度
        y="latitude",                     # 縦軸に緯度
        alpha=0.6,                         # 点の重なりを見やすくする
        s=housing["population"] / 100,    # 人口を点サイズへ反映する
        label="population",
        c="median_house_value",           # 住宅価格を色へ反映する
        cmap="jet",                       # 色の対応表を指定する
        colorbar=True,                     # 色の凡例を右側へ表示する
        figsize=(12, 8),
    )
plt.legend()
# 表示範囲を固定して、地理的な広がりを比較しやすくする
plt.axis([-125, -113.5, 32, 43])
plt.show()

# 所得と住宅価格の関係だけを切り出して、主要因の候補を確認する
# kind="scatter" で散布図を選び、alpha で点の重なりを読みやすくする
housing.plot(kind="scatter", x="median_income", y="median_house_value", alpha=0.8)
plt.show()

データ検索・結合

この章では、欲しい行を短く見つけるための並べ替えと抽出を先に整理し、その後に複数の表を結合する流れへ進みます。

並べ替えと抽出を短く書く

この節では、「欲しい行や列へ最短でたどり着く」ことを重視します。集計や可視化の前に対象を素早く絞れると、試行回数が増えて分析の質も上がりやすくなります。

インデックスを主役にした並べ替え

sort_values() は列値基準、sort_index() は行ラベルや列ラベル基準で並べ替える。set_index() を使うとキー列中心の操作に切り替えやすい。
注文日や商品コードをインデックスへ置いておくと、日付順確認や商品別の切り出しが短く書ける。小売の明細表では、並べ替えの基準が列なのかキーなのかを明確にするだけで可読性が上がる。

import pandas as pd

df = pd.DataFrame(
    {
        "key": ["b", "c", "a"],
        "data1": [1, 3, 2],
        "data2": [10, 30, 20],
    }
)

print(df.sort_values("data1"))

df2 = df.set_index("key")
print(df2.sort_index())
print(df2.sort_index(axis=1, ascending=False))

複数キーと欠損位置を指定して sort_values() する

sort_values() は複数列・昇降順の混在・na_position まで指定できる。カテゴリ列と数値列を同時に整列させたいときに便利である。
店舗別にまず並べ、その中で売上順に見たい、さらに欠損行はレビュー対象として先頭へ寄せたい、といった実務の確認作業に向いている。

import numpy as np
import pandas as pd

df = pd.DataFrame(
    {
        "Color": ["blue", np.nan, "red", "blue", "green", np.nan],
        "ID": [101, 104, 102, 99, 120, 98],
        "Quality": [4, 7, 5, 9, 6, 3],
    }
)

print(df.sort_values(by=["Color", "ID"], ascending=[True, False], na_position="first"))

filter() / query() / get_loc() で抽出の記述を短くする

filter() は列名やインデックス名の部分一致抽出、query() は条件式を文字列で書きたいとき、get_loc() はラベルから整数位置を取得して iloc へ渡したいときに便利である。
EC の商品属性表では、色関連列だけ抜く、サイズ条件を満たす SKU だけ残す、特定列の位置を即座に取る、といった操作が頻繁に出るため、短く書けると試行錯誤が速くなる。

import pandas as pd

df = pd.read_csv("Example_without_index.csv")

print(df.filter(like="or", axis=1).head(3))
print(df.query("9 < Height & Width < 3").head())

color_idx = df.columns.get_loc("Color")
print(df.iloc[0:3, color_idx])

複数の表をつなぐ

表結合は、前処理の中でも特に事故が起きやすい操作です。重要なのは関数名より、「どのキーで」「どの行を残し」「どの欠損を許すか」を明示して、結合後の件数や欠損増加を必ず確認することです。

concat() と merge() の役割分担

concat() は単純な縦横連結、merge() はキーに基づく結合。how="outer" を使うと片側にしかないレコードも残せる。
実務では、月別ファイルの縦結合は concat()、注文台帳と商品マスタの結合は merge() と覚えると混乱しにくい。特に outer は、片側にしか存在しない商品や顧客を落とさず異常検知したいときに役立つ。

import pandas as pd

df1 = pd.DataFrame({"city": ["Tokyo", "Osaka"], "population": [14, 9]})
df2 = pd.DataFrame({"city": ["Tokyo", "Nagoya"], "gdp": [100, 60]})

print(pd.concat([df1, df1], ignore_index=True))
print(pd.concat([df1, df2], axis=1))

# inner は両方に存在するキーだけ残す
print(pd.merge(df1, df2, on="city", how="inner"))

# outer は片側にしかないキーも残し、欠損を伴って結合する
print(pd.merge(df1, df2, on="city", how="outer"))

日付データの処理

この章では、日時列を自分で作るところから始め、DatetimeIndex へ載せ替え、再集計し、最後に時間の流れを図で確認するところまでをまとめます。

日時列を作って扱える形にする

時系列処理で最初にやるべきことは、日時列を「文字列」ではなく「時間として計算できる型」へ変えることです。ここが曖昧なままだと、月次集計やラグ特徴量の作成で毎回遠回りになります。

map() と to_datetime() で日時列を組み立てる

Series.map() は辞書や関数での一括変換に向く。年月を別列で持つデータでも、文字列結合と pd.to_datetime() を使えば日時列を手早く作成できる。
小売では「年」「月」が別列で渡される POS データ、EC では配信月や契約更新月が別列のレポートが多く、最初に日時列へ直しておくと集計粒度の変更がしやすい。

import pandas as pd
import seaborn as sns

flights = sns.load_dataset("flights")

month_dict = {
    "Jan": 1, "Feb": 2, "Mar": 3, "Apr": 4,
    "May": 5, "Jun": 6, "Jul": 7, "Aug": 8,
    "Sep": 9, "Oct": 10, "Nov": 11, "Dec": 12,
}

flights["month_n"] = flights["month"].map(month_dict)
flights["year_month"] = flights["year"].map(lambda x: f"{x}/") + flights["month_n"].astype(str)
flights["date"] = pd.to_datetime(flights["year_month"])

print(flights[["year", "month", "month_n", "date"]].head())

DatetimeIndex・resample()・shift() で時系列特徴量を作る

DatetimeIndex にすると年月範囲での抽出や resample() による再集計が自然に書ける。shift() と組み合わせれば、前月比のようなラグ特徴量も簡単に追加できる。
日次売上を週次へまとめる、前日比や前週比を見る、セール前後の変化を比較する、といった小売・EC の典型タスクでは、この一連の流れがそのまま使える。

import pandas as pd
import seaborn as sns

flights = sns.load_dataset("flights")

month_dict = {
    "Jan": 1, "Feb": 2, "Mar": 3, "Apr": 4,
    "May": 5, "Jun": 6, "Jul": 7, "Aug": 8,
    "Sep": 9, "Oct": 10, "Nov": 11, "Dec": 12,
}

flights["month_n"] = flights["month"].map(month_dict)
flights["date"] = pd.to_datetime(
    flights["year"].astype(str) + "/" + flights["month_n"].astype(str)
)

# DatetimeIndex にすると、期間抽出と resample が自然に書ける
flights = flights.set_index("date")

# YE は年末単位で再集計する指定である
yearly = flights.resample("YE")["passengers"].mean()

# shift(1) は 1 期前の値をずらして作るので、前月比の基礎になる
flights["prev_month_passengers"] = flights["passengers"].shift(1)
flights["difference"] = flights["passengers"] - flights["prev_month_passengers"]

print(flights.loc["1949":"1950-07", ["passengers", "difference"]].head())
print(yearly.head())

時系列の流れを図で確認する

最後は、作った日時列や特徴量が本当に自然に見えるかを図で確認します。時系列では、数表だけで正しいと思っても、描いてみると欠損補完や集計粒度の違和感が一目で分かることが多いです。

seaborn.lineplot() と DatetimeIndex を組み合わせて時系列を描く

lineplot() は時系列の推移確認に向いており、Axes を明示すると複数図の比較も整理しやすい。日時列と組み合わせると、Pandas の時系列処理との相性がよい。
EC の日別注文件数や、小売の週次売上を確認するときは、まず線で流れを見てから外れた期間を深掘りするのが基本である。マーカー付きにすると、どの時点の実測値かも追いやすい。

import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns

flights = sns.load_dataset("flights")
month_dict = {
    "Jan": 1, "Feb": 2, "Mar": 3, "Apr": 4,
    "May": 5, "Jun": 6, "Jul": 7, "Aug": 8,
    "Sep": 9, "Oct": 10, "Nov": 11, "Dec": 12,
}

flights["month_n"] = flights["month"].map(month_dict)
flights["date"] = pd.to_datetime(flights["year"].astype(str) + "/" + flights["month_n"].astype(str))

fig, axes = plt.subplots(1, 2, figsize=(10, 4), tight_layout=True)

# 線と点を両方描くと、全体トレンドと各月の観測値を同時に読める
sns.lineplot(data=flights, x="date", y="passengers", marker=".", linewidth=1, ax=axes[0])
axes[0].set_title("passengers")

# 線を消してマーカーだけにすると、観測点の間隔を確認しやすい
sns.lineplot(data=flights, x="date", y="passengers", marker="X", linewidth=0, ax=axes[1])
axes[1].set_title("markers only")
plt.show()
0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?