「dataframe」は統計データ分析の文脈で扱う重要概念のひとつ。 本ページでは「dataframe」を取り巻く中核キーワードを以下にチップで一覧化する。 各キーワードは関連する概念・手法・道具立てを含み、 文献検索や学習計画の起点になる。
これらのキーワードは「dataframe の理解 → 適用 → 検証」のプロセスを構成する。 各章で詳しく解説する。
🍰 まずはやさしく
エクセルの表のようなものです。
データをまとめて管理するために使います。
部活の出席簿のような表をイメージしてください。
この章では基本的な使い方を学びます。
DataFrame ── pandasの表形式データ構造
Series(1次元、 型統一)。 DataFrame は Series の辞書loc/iloc)/フィルタ(query)/集約(groupby)/結合(merge)🍰 まずはやさしく
データを読み込むとすぐに使います。
分析作業のほとんどで利用する道具です。
スマホの連絡先リストのような形式です。
データの選び方やまとめ方を学びます。
SSDSE のCSVを読み込んだ瞬間からあなたは DataFrame を触っています。 全データサイエンス作業の8割はこの上での操作と言って過言ではありません。
本ページでは SSDSE-B-2026 を pd.read_csv() で読み込み、 47 行 × 100 超列の DataFrame に対する loc / iloc / query / merge / groupby / pivot_table 等を実例で示し、 Series との往復関係を明示する。
「dataframe」は 行 (observation) × 列 (variable) の 2 次元ラベル付きテーブルで、 pandas における基本データ構造である。 異なる dtype を列ごとに保持できる点で numpy 2D 配列と異なり、 SSDSE-B-2026 のような混合型データ (都道府県名 + 数値列) を扱うのに最適。 本ページでは Series との関係、 indexing (loc/iloc)、 join/merge/groupby を扱う。
🍰 まずはやさしく
縦と横に並んだデータの集まりです。
項目ごとに情報を整理するために使います。
都道府県ごとの人口などの表が例です。
表の構造を詳しく見ていきましょう。
都道府県データを例に:
都道府県 人口 高齢化率 死亡率
0 北海道 5224614 32.5 12.1
1 青森県 1237984 34.6 14.2
2 岩手県 1210534 34.2 13.5
…
これが DataFrame。 行=都道府県、 列=変数、 インデックスは番号 or 都道府県名。
カラム集合 C = {A1101(総人口), A1303(65歳以上人口), A4101(出生数), ...}
│
▼
┌───────┬────────┬───────┬─────┐
Prefecture │ A1101 │ A1303 │ A4101 │ ... │ ← dtypes: int64, int64, int64
────── ├───────┼────────┼───────┼─────┤
北海道 │5092000 │1681000 │ 24430 │ ... │
青森県 │1184000 │ 417000 │ 5696 │ ... │
東京都 │14086000│3205000 │ 86348 │ ... │
⋮ │ ⋮ │ ⋮ │ ⋮ │ ⋮ │
沖縄県 │1468000 │ 350000 │ 12549 │ ... │
└───────┴────────┴───────┴─────┘
│
▼
各列 = Series (1次元, 型統一)
全体 = Series の辞書
操作の代数(SSDSE-B-2026, 2023年):
σ (selection) → df[df['A1101']>1e6]
π (projection) → df[['A1101','A1303']]
⋈ (join) → df1.merge(df2, on='Prefecture')
Γ (aggregate) → df.groupby('地域')['A1101'].sum()
SSDSE-B は年度の整数を持つだけですが、 多くの実務データは日次や分次の時刻列を含みます。 pd.to_datetime で datetime64 化すると、 リサンプリング(df.resample('M').mean())、 ローリング(df.rolling('30D').mean())、 期間スライス(df.loc['2023-04':'2023-09'])が利用可能になります。
マルチインデックスは「都道府県 × 年度」のような複合主キー的データに使うと強力で、 df.set_index(['Prefecture','SSDSE-B-2026']) としておけば df.loc[('東京都', 2023)] で1点を直接取れます。 ただし可読性が低下しやすいので、 アドホック分析では reset_index で平坦化して扱う方が便利な場合も多いです。
表形式データを「縦持ち(long)」⇔「横持ち(wide)」で変換するのが melt / pivot / stack / unstack の4兄弟です。 機械学習モデルへの入力は wide、 可視化(seaborn)への入力は long が多い、 と覚えておくと迷いません。 SSDSE-B はもともと long 形式に近いので、 必要に応じて pivot で wide にして使うのが標準パターンです。
SSDSE-B-2026 のような都道府県データは、 ほぼ全てのデータサイエンス入門で登場します。 ここでは「読み込み → 整形 → 集約 → 可視化 → モデリング前処理」の典型フローを通しで示します。
1 2 3 4 5 6 7 8 9 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1], dtype={'Code': str}, # 先頭ゼロ保持 ) print(df.shape) print(df.columns.tolist()[:10]) |
1 2 3 4 5 6 | df23 = (df[df['SSDSE-B-2026']==2023] .set_index('Prefecture') .drop(columns=['SSDSE-B-2026','Code'], errors='ignore') .astype({'A1101':'int64'})) print(df23.shape) # (47, 109) print(df23.head()) |
1 2 3 4 5 6 | df23 = df23.assign( 高齢化率 = lambda d: d['A1303'] / d['A1101'] * 100, 出生率 = lambda d: d['A4101'] / d['A1101'] * 1000, # 人口千人あたり出生数 生産年齢比 = lambda d: d['A1302'] / d['A1101'] * 100, ) print(df23[['A1101','高齢化率','生産年齢比']].describe()) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | # ── ここから下の書き方見本で使うデータを用意します ── # SSDSE-B は 1 行目が英字コード(A1101 など)、2 行目が日本語名。 # 見本では英字コードを使うので、1 行目を見出しにして読み込みます。 import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=0) df = df[df['Code'].astype(str).str.match(r'^R\d{5}$', na=False)].copy() # 1 行目を見出しにすると数値の列も文字列で入ってくるので、まとめて数値に直す for _c in df.columns: if _c not in ('Code', 'Prefecture'): df[_c] = pd.to_numeric(df[_c], errors='coerce') # 見本の中で使う派生列(地域ブロック・高齢化率)と、結合用のもう 1 枚 _region = {'北海道':'北海道','青森県':'東北','岩手県':'東北','宮城県':'東北','秋田県':'東北', '山形県':'東北','福島県':'東北','茨城県':'関東','栃木県':'関東','群馬県':'関東', '埼玉県':'関東','千葉県':'関東','東京都':'関東','神奈川県':'関東'} df['地域'] = df['Prefecture'].map(_region).fillna('その他') df['高齢化率'] = df['A1303'] / df['A1101'] * 100 df2 = df[['Prefecture', 'A1101']].copy() # この見本で使う 2023 年度・47 都道府県の表(都道府県名を索引にする) df23 = (df[df['SSDSE-B-2026'] == 2023] .set_index('Prefecture') .drop(columns=['SSDSE-B-2026', 'Code'], errors='ignore')) region_map = { '北海道':'北海道', '青森県':'東北','岩手県':'東北','宮城県':'東北','秋田県':'東北','山形県':'東北','福島県':'東北', '茨城県':'関東','栃木県':'関東','群馬県':'関東','埼玉県':'関東','千葉県':'関東','東京都':'関東','神奈川県':'関東', '新潟県':'中部','富山県':'中部','石川県':'中部','福井県':'中部','山梨県':'中部','長野県':'中部','岐阜県':'中部','静岡県':'中部','愛知県':'中部', '三重県':'近畿','滋賀県':'近畿','京都府':'近畿','大阪府':'近畿','兵庫県':'近畿','奈良県':'近畿','和歌山県':'近畿', '鳥取県':'中国','島根県':'中国','岡山県':'中国','広島県':'中国','山口県':'中国', '徳島県':'四国','香川県':'四国','愛媛県':'四国','高知県':'四国', '福岡県':'九州','佐賀県':'九州','長崎県':'九州','熊本県':'九州','大分県':'九州','宮崎県':'九州','鹿児島県':'九州','沖縄県':'九州', } df23['地域'] = df23.index.to_series().map(region_map) region_agg = df23.groupby('地域').agg( 人口=('A1101','sum'), 平均高齢化率=('高齢化率','mean'), 県数=('A1101','count'), # 集計後の列名は 人口/平均高齢化率/県数。'A1101' はもう存在しない ).sort_values('人口', ascending=False) print(region_agg) |
1 2 3 4 | panel = (df.pivot_table(index='Prefecture', columns='SSDSE-B-2026', values='A1101', aggfunc='sum') .assign(変化率_10年=lambda d: (d[2023]/d[2013]-1)*100)) print(panel[[2013, 2018, 2023, '変化率_10年']].sort_values('変化率_10年').head(10)) # 減少率トップ10は秋田・青森・岩手・高知・山形・徳島・長崎・新潟・和歌山・福島 |
1 2 3 4 | import seaborn as sns sub = df23[['A1101','高齢化率','A4101','L3221']] g = sns.pairplot(sub.dropna(), corner=True, diag_kind='kde') g.fig.suptitle('SSDSE-B-2026 (2023) 都道府県の散布図行列', y=1.02) |
このフロー全体が「DataFrame に対する Split-Apply-Combine」の繰り返しです。 慣れると新しいデータでも同じパターンで攻略できます。
下の3つの実験は、 SSDSE-B-2026(2023年)の実測値から6県を抜粋した小さな DataFrame です(A1101=総人口、 A1303=65歳以上人口、 単位は人)。 pandas 固有の3つの概念 —— (1) 行index・列名ラベルによる loc アクセス、 (2) boolean mask による行の抽出、 (3) 列演算のブロードキャスト —— を、 図とコードが連動する形で体感できます。 なお、 表データ一般のソート・フィルタ・集計の実験は 表データ のページにあります。 このページは「pandas の DataFrame ならでは」の操作に絞ります。
行ラベル(index=都道府県名)と列名(columns)をセレクタで選ぶと、 該当するセル・行・列がハイライトされます。 表のセルを直接タップ/クリックしてもOK(index 列をタップ=行選択、 列見出しをタップ=列選択)。 「:」は「すべて」の意味です。
要点loc はラベル、 iloc は整数位置。 上のコード欄には両方の書き方が並びます。 「行だけ指定すると Series(横に寝た1行)」「列だけ指定すると Series(縦の1列)」「両方 : なら DataFrame 全体」と、 返ってくる型が指定の仕方で変わることを確認してください。
スライダーでしきい値を動かすと、 各行が True / False に判定され(=boolean mask、 長さ6の Series)、 False の行が薄くなります。 mask を df[...] に渡すと True の行だけが残る —— これが pandas のフィルタリングの正体です。 高齢化率は A1303 ÷ A1101 × 100 の実測値です。
要点比較演算 df['高齢化率'] > 30 自体が「答え」ではなく、 True/False の列(mask)を作る中間生成物です。 mask 同士は &(かつ)や |(または)で合成でき、 その際は (df['A'] > 1) & (df['B'] < 2) のように括弧が必須(演算子の優先順位のため)です。
新列「高齢化率」を作ります。 「1行ずつ」ボタンは for ループのイメージ(1回押すごとに1行だけ計算)、 「一括ブロードキャスト」は pandas の列演算 —— 1つの式で全行が同時に計算されます。 実データ分析ではこの差が「30分 vs 1秒」になります。
要点df['A1303'] / df['A1101'] は「列(Series)÷ 列(Series)」。 pandas はindex を揃えて要素ごとに割り算し、 × 100 のようなスカラーは全要素に自動で行き渡ります(ブロードキャスト)。 df['新列'] = 式 と書くだけで6行ぶん(実データなら47行・564行ぶん)が一括で埋まります。
上の3実験に共通する見方は2つです。 第一に DataFrame はラベル付きの2次元表: 行には index(実験①では都道府県名)、 列には columns(A1101 など)という名前が付いていて、 df.loc[行ラベル, 列ラベル] で位置番号を数えずに値へ届く。 第二に DataFrame は列指向: 実体は「dtype の揃った1次元 Series を横に束ねたもの」で、 だから df['A1101'] の取り出しや実験③の列演算が速く自然に書けます。 「行の集まり」と見るより「列の束」と見る方が、 pandas のコードは圧倒的に読み書きしやすくなります。
df.loc['東京都','A1101'](ラベル)と df.iloc[1, 0](位置)は書き方が別物。 さらにスライスの端の扱いが逆で、 loc['北海道':'愛知県'] は両端を含み、 iloc[0:2] は終端を含みません。 index が整数(0,1,2,…)のときに最も事故が起きるので、 迷ったら都道府県名のような文字列 index を張るのが安全です。df[mask] で絞った結果は「ビューかコピーか不定」。 絞った後に代入すると元の df に反映されないことがあります。 続けて加工するなら df2 = df[mask].copy()、 部分代入なら df.loc[mask, '列'] = 値 とloc で一発で書くのが正解です。df.loc['東京都'] が Series を返すのは index が一意だから。 もし index に同じラベルが2回あると、 同じコードがDataFrame(複数行)を返して型が変わり、 後段の処理が壊れます。 set_index の後は df.index.is_unique の確認を習慣に。①〜③の組み合わせが分かれば、 次の3つは自然な拡張です。 groupby は「mask で1グループずつ絞って集計」を全グループぶん自動化したもの(Split-Apply-Combine)。 merge(データ結合) は「ラベルで行を対応付ける」loc の考え方を2つの DataFrame の間に広げたもの。 そして 時系列 index は index に日時(datetime64)を張ることで、 df.loc['2023-01':'2023-06'] のような期間スライスや resample('MS') の集約が使えるようになる拡張です。 前提となる pandas・NumPy、 実務で必ず出会う 欠損値 も合わせて確認してください。
🍰 まずはやさしく
行と列でできたデータの構造です。
正しく計算や分析をするために定義します。
買い物リストの品名と金額のような構成です。
表を形作る3つの要素について学びます。
DataFrameの3層の構造:
dataframe の定義や代表的な数式を以下に示す。 数式の各記号の意味は次節で言葉に翻訳する。
dataframe は文脈に応じて複数の定式化があるが、 教育目的では最も基本的な形を抑えることが重要。 具体的な値での計算例は後続セクションを参照。
$$\text{dataframe}: f(\mathbf{X}, \boldsymbol{\theta}) \to y$$
記号の対応はこうです。 $I$ は行ラベルの集合(インデックス。 SSDSE なら 47 都道府県コード)、 $C$ は列名の集合(A1101、 A1303 など 112 列)、 $V$ が値の集合です。 $t_c$ は列 $c$ の型で、 ここが DataFrame と 2 次元配列の決定的な違い——型は列ごとに決まり、 行ごとには決まりません。 $v_{i,c}$ は「行 $i$・列 $c$ の値」で、 df.loc[i, c] がこれに当たります。 $D = \{(i, c, t_c, v_{i,c})\}$ という 4 つ組の集合として書けることは、 「DataFrame は行 × 列の表であると同時に、 列ごとに型を持つ辞書でもある」ことを表しています。 df.dtypes が返すのがまさに $\{(c, t_c)\}$ です。
DataFrame は数学的にも厳密に定義できる「3つ組」です。 $$\mathrm{DataFrame} = (I,\ C,\ V)$$ ここで $I = \{i_1, i_2, \dots, i_n\}$ はインデックス集合(行ラベル、 観測の識別子)、 $C = \{c_1, c_2, \dots, c_m\}$ はカラム集合(列ラベル、 変数の識別子)、 $V \in \mathbb{R}^{n \times m}$(または各列が独自の型を持つ「型付き多態配列」) は値の行列です。 SSDSE-B-2026 で言えば、 $n=47$(都道府県)、 $m \approx 110$(変数数)、 $V_{i,j}$ は第 $i$ 県の第 $j$ 変数の値となります。
$I$ は通常「整数の連番 ($0, 1, \dots, n-1$)」または「意味のあるラベル(都道府県名、 日付、 顧客ID)」のいずれかです。 重要な性質は一意性(重複しないことが原則)、 順序保持(行の並びが意味を持つ場合がある)、 ハッシュ可能性(loc で高速参照するため)です。 pandas では df.set_index('都道府県') でインデックスを差し替えると、 $i \in I$ に対し df.loc[i] が $O(\log n)$ または $O(1)$(ハッシュ)で動きます。 逆に reset_index() でインデックスを通常列に戻せます。 マルチインデックス(タプル)にすれば $I \subseteq A \times B$ の階層構造も持てます。
$C$ も $I$ と同様にラベルの集合ですが、 さらに各 $c \in C$ には型 $\tau(c) \in \{\mathtt{int64},\mathtt{float64},\mathtt{object},\mathtt{datetime64},\mathtt{category},\mathtt{bool}\}$が紐づきます。 例えば SSDSE-B-2026 では「総人口」は int64、 「高齢化率(%)」は float64、 「都道府県名」は object(実体は str)になります。 型の選択は計算速度・メモリ・正確性に直結します(郵便番号を int で読むと先頭0が消える、 大カテゴリは category で 10倍速くなる、 など)。 列単位で型が独立している点が NumPy の単一 dtype ndarray との最大の違いです。
値 $V$ は「行 × 列」の二次元構造で、 $V_{i,j}$ をセルと呼びます。 pandas は内部的に「列ごとに 1次元 ndarray」 (BlockManager) を持つため、 列方向の演算 df['col'].mean() は NumPy の C ベクトル化で高速($\mu = \frac{1}{n}\sum_{i=1}^{n} V_{i,j}$)、 行方向ループ(iterrows)は Python 側ループで桁違いに遅い、 という非対称性が生まれます。 欠損は NaN(float の IEEE 754 NaN)または pd.NA(型に依存しない欠損)で表現され、 集約関数のデフォルトは skipna=True(NaN を除外して計算)です。
DataFrame の操作は関係代数(SQL)と数学的に近い構造を持ちます。 選択 $\sigma$: $\sigma_{\text{条件}}(\mathrm{df}) = \{i \in I \mid \text{条件}(V_i) = \mathrm{True}\}$ が df[df['A1101']>1e6]。 射影 $\pi$: $\pi_{C'}(\mathrm{df})$ が df[['A1101','A1303']]。 結合 $\bowtie$: $\mathrm{df}_1 \bowtie_{\text{key}} \mathrm{df}_2$ が merge。 集約 $\Gamma$: $\Gamma_{\text{key},f}(\mathrm{df})$ が groupby + agg。 この4操作を組み合わせることで、 ほぼ全てのデータ加工が表現できます(チューリング完全ではないが、 解析的タスクには十分豊か)。
DataFrame の背景には Edgar F. Codd (1970) が提唱した関係モデルがあります。 Codd の関係 $R \subseteq D_1 \times D_2 \times \dots \times D_m$ は「ドメインの直積の部分集合」で、 これが現代的な「行 × 列の表」の祖先です。 関係代数は次の 6 つの基本演算で成り立ちます:
| 演算 | 記号 | pandas での対応 | SQL |
|---|---|---|---|
| 選択 | $\sigma_{P}(R)$ | df[df['x']>0] | WHERE |
| 射影 | $\pi_{A,B}(R)$ | df[['a','b']] | SELECT |
| 和 | $R \cup S$ | pd.concat([r,s]) | UNION |
| 差 | $R - S$ | r[~r.index.isin(s.index)] | EXCEPT |
| 直積 | $R \times S$ | r.merge(s, how='cross') | CROSS JOIN |
| 改名 | $\rho_{A/B}(R)$ | df.rename(columns={'b':'a'}) | AS |
これらに集約 $\Gamma$ と結合 $\bowtie$ を加えれば、 SQL の全機能をカバーします。 pandas はこの関係代数を Python の式言語として実装したもの、 と見ると全体像がすっきり捉えられます。 ただし pandas は順序を保持するため、 関係代数の「集合(順不同)」より強い構造を持ちます。 これが時系列処理を自然にこなせる理由です。
df[df.a>0]['b'] = 1 ❌ → df.loc[df.a>0, 'b'] = 1 ✅for i,r in df.iterrows(): df.loc[i,'c'] = r.a*r.b ❌ → df['c'] = df['a']*df['b'] ✅for x in xs: df = df.append({...}) ❌(O(n²))→ リストに溜めて最後に pd.DataFrame(rows) ✅for i,r in df.iterrows(): match = df2[df2.k==r.k] ❌ → df.merge(df2, on='k') ✅df['x'].fillna(0).mean() ❌(下振れ)→ df['x'].mean()(skipna=True) ✅astype('category') ✅(メモリ 1/10)dtype={'郵便番号': str} 明示 ✅.copy() で明示化 ✅1 2 3 4 5 6 7 | df23.groupby('地域').agg( 人口合計=('A1101','sum'), 人口平均=('A1101','mean'), 高齢化率最大=('高齢化率','max'), 高齢化率最小=('高齢化率','min'), 県数=('A1101','count'), ) |
1 2 3 | df23['地域内順位'] = df23.groupby('地域')['A1101'].rank(ascending=False) top2 = df23[df23['地域内順位']<=2].sort_values(['地域','地域内順位']) print(top2[['A1101','地域内順位']]) |
1 2 3 4 | panel = df.pivot_table(index='Prefecture', columns='SSDSE-B-2026', values='A1101') panel = panel.interpolate(method='linear', axis=1) # 年度で線形補間。 端点は前方/後方フィルでも可 panel = panel.bfill(axis=1).ffill(axis=1) |
1 2 3 4 | num_cols = df23.select_dtypes('number').columns z = (df23[num_cols] - df23[num_cols].mean()) / df23[num_cols].std() print(z.head()) # scikit-learn の StandardScaler でも同じ |
1 2 3 4 5 6 7 8 9 10 11 12 | import pandas as pd # ── この抜粋だけで動くように、突き合わせる 2 つの表を作る ── _d = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) _d = _d[_d['SSDSE-B-2026'] == 2023] df1 = _d[['Prefecture', 'A1101']].iloc[:45] # わざと 2 県欠けた表 df2 = _d[['Prefecture', 'A4101']].iloc[2:] # わざと先頭 2 県が欠けた表 m = df1.merge(df2, on='Prefecture', how='outer', indicator=True) print(m['_merge'].value_counts()) # both / left_only / right_only がそれぞれ何件あるか可視化 print(m[m['_merge']!='both']) # 抜けている県を確認 |
SSDSE-B-2026 の 47 都道府県 × 110 変数を縦横無尽に動かせるようになれば、 ほとんどの公的統計データはこのままの DataFrame 操作で攻略できます。 「データを読む → 表にする → 集める → 結ぶ → 描く」 — この 5 動詞を呼吸のように使いこなせるのが、 データサイエンティストの第一段階です。
DataFrame の結合は、 関係代数の結合演算 $R_1 \bowtie_{k} R_2$ に対応します。 SSDSE-B-2026 を扱うときも、 「人口データ × 気候データ × 経済データ」を都道府県名でつなぐ場面が頻出します。 ここでは merge の全パターンを SSDSE 風データで例示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | # ── ここから下の書き方見本で使うデータを用意します ── # SSDSE-B は 1 行目が英字コード(A1101 など)、2 行目が日本語名。 # 見本では英字コードを使うので、1 行目を見出しにして読み込みます。 import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=0) df = df[df['Code'].astype(str).str.match(r'^R\d{5}$', na=False)].copy() for _c in ['A1101', 'A1303', 'SSDSE-B-2026']: df[_c] = pd.to_numeric(df[_c], errors='coerce') # 見本の中で使う派生列(地域ブロック・高齢化率)と、結合用のもう 1 枚 _region = {'北海道':'北海道','青森県':'東北','岩手県':'東北','宮城県':'東北','秋田県':'東北', '山形県':'東北','福島県':'東北','茨城県':'関東','栃木県':'関東','群馬県':'関東', '埼玉県':'関東','千葉県':'関東','東京都':'関東','神奈川県':'関東'} df['地域'] = df['Prefecture'].map(_region).fillna('その他') df['高齢化率'] = df['A1303'] / df['A1101'] * 100 df2 = df[['Prefecture', 'A1101']].copy() pop = df[df['SSDSE-B-2026']==2023][['Prefecture','A1101']] # 47件 climate = pd.read_csv('data/climate.csv')[['Prefecture','年間平均気温']] # 46件(沖縄欠落と仮定) inner = pop.merge(climate, on='Prefecture', how='inner') # 46件 print(inner.shape) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import pandas as pd # ── この抜粋だけで動くように、結合する 2 つの表を作る ── _d = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) _d = _d[_d['SSDSE-B-2026'] == 2023] pop = _d[['Prefecture', 'A1101']].rename(columns={'A1101': '総人口'}) # 沖縄県だけ気温データが欠けている状況を作る climate = (_d[['Prefecture', 'B4101']].rename(columns={'B4101': '年間平均気温'}) .query("Prefecture != '沖縄県'")) left = pop.merge(climate, on='Prefecture', how='left') # 47件(沖縄の気温は欠損) _miss = left[left['年間平均気温'].isna()] # 欠損はそのまま出すと NaN と表示されて「計算が壊れた」ように見えるので、 # 右側に行が無かったことが分かる形にして表示する print(f'右側に相手が見つからなかった行: {len(_miss)} 件') print(_miss.assign(年間平均気温='(右の表に無し)').to_string(index=False)) |
1 2 3 | full = pop.merge(climate, on='Prefecture', how='outer', indicator=True) print(full['_merge'].value_counts()) # both=46, left_only=1 (沖縄), right_only=0 |
1 2 3 4 5 6 7 8 9 10 11 12 13 | # ── この抜粋で使うデータを用意します(英字の項目コードで読み込み)── import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=0) df = df[df['Code'].astype(str).str.match(r'^R\d{5}$', na=False)].copy() for _c in df.columns[3:]: df[_c] = pd.to_numeric(df[_c], errors='coerce') df = df[df['SSDSE-B-2026'] == df['SSDSE-B-2026'].max()] economy = df[['Prefecture','SSDSE-B-2026','A4101']] demo = df[['Prefecture','SSDSE-B-2026','A1101','A1303']] panel = economy.merge(demo, on=['Prefecture','SSDSE-B-2026']) panel['出生率'] = panel['A4101'] / panel['A1101'] |
注意: 都道府県名は表記揺れ(東京都/東京、 北海道/北海道庁)が起きやすい。 必ず .str.strip().str.replace('庁','') で正規化してから merge。
Split-Apply-Combine は Hadley Wickham (2011) が R の plyr 設計で定式化した概念で、 pandas の groupby もこの3段階で動きます。
df.groupby('地域') はキーごとの DataFrameGroupBy オブジェクトを返す。 実際の分割はまだ起きていない(遅延評価)。.agg(np.mean) や .apply(func) で各グループに関数を適用。 ここで内部的にループ。| メソッド | 関数の入力 | 関数の出力 | 結果の形 |
|---|---|---|---|
agg | Series | スカラー | グループ数 × 集約数 |
transform | Series | Series(同サイズ) | 元と同じ shape |
apply | DataFrame | 何でも | 柔軟(最も遅い) |
filter | DataFrame | bool | 条件を満たすグループの行 |
1 2 3 4 5 6 7 8 | # transform: 全国平均との差(z スコア風) df23['人口_全国平均との差'] = df23['A1101'] - df23['A1101'].mean() # 地域内で中央化 df23['人口_地域中央化'] = df23['A1101'] - df23.groupby('地域')['A1101'].transform('mean') # filter: 平均高齢化率が 30 を超える地域だけ high_aging = df23.groupby('地域').filter(lambda g: g['高齢化率'].mean() > 30) print(high_aging['地域'].unique()) # 東北、四国 など |
DataFrame と可視化は表裏一体。 df.plot() はワンライナーで便利ですが、 まじめなグラフは matplotlib / seaborn / plotly を使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import os os.makedirs('outputs', exist_ok=True) # 保存先のフォルダを作っておく import matplotlib.pyplot as plt import seaborn as sns # 47都道府県の人口 vs 高齢化率 plt.figure(figsize=(8,6)) sns.scatterplot(data=df23.reset_index(), x='A1101', y='高齢化率', hue='地域', size='A4101', sizes=(40, 400)) plt.xscale('log') # 人口は log で見ると見やすい for _,r in df23.reset_index().iterrows(): plt.annotate(r['Prefecture'], (r['A1101'], r['高齢化率']), fontsize=8) plt.title('SSDSE-B-2026 (2023) 都道府県の人口と高齢化率') plt.tight_layout(); plt.savefig('outputs/pop_aging.png', dpi=120) |
人口は log スケールにすると東京の超巨大ぶりが緩和される。 高齢化率は線形のままで OK。 県名注釈で「外れ値」(沖縄=若い、 秋田=高齢)が一目瞭然になる。
本番運用の DataFrame は必ずテストを書くのが鉄則です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | # ── ここから下の書き方見本で使うデータを用意します ── # SSDSE-B は 1 行目が英字コード(A1101 など)、2 行目が日本語名。 # 見本では英字コードを使うので、1 行目を見出しにして読み込みます。 import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=0) df = df[df['Code'].astype(str).str.match(r'^R\d{5}$', na=False)].copy() for _c in ['A1101', 'A1303', 'SSDSE-B-2026']: df[_c] = pd.to_numeric(df[_c], errors='coerce') # 見本の中で使う派生列(地域ブロック・高齢化率)と、結合用のもう 1 枚 _region = {'北海道':'北海道','青森県':'東北','岩手県':'東北','宮城県':'東北','秋田県':'東北', '山形県':'東北','福島県':'東北','茨城県':'関東','栃木県':'関東','群馬県':'関東', '埼玉県':'関東','千葉県':'関東','東京都':'関東','神奈川県':'関東'} df['地域'] = df['Prefecture'].map(_region).fillna('その他') df['高齢化率'] = df['A1303'] / df['A1101'] * 100 df2 = df[['Prefecture', 'A1101']].copy() import pandas as pd import pandas.testing as pdt def make_panel(year): df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) return df[df['SSDSE-B-2026']==year].set_index('Prefecture')[['A1101']] panel = make_panel(2023) # 1. shape の検証 assert panel.shape == (47, 1), f'47都道府県のはずが {panel.shape}' # 2. 主キー一意性 assert panel.index.is_unique # 3. 欠損なし assert panel.isna().sum().sum() == 0 # 4. 値の範囲 assert (panel['A1101'] >= 0).all() assert panel['A1101'].sum() > 1.2e8 and panel['A1101'].sum() < 1.3e8 # 1.2-1.3億 # 5. 既知の事実を確認 assert panel.loc['東京都','A1101'] > panel.loc['鳥取県','A1101'] # 6. 期待結果との一致(リグレッション) expected = pd.read_pickle('tests/expected_2023.pkl') pdt.assert_frame_equal(panel, expected) |
この6項目を pytest に登録しておけば、 上流データが変わったときに自動で気づける。
SSDSE-B-2026 を「2023年だけ抽出 → 地域別人口合計」する処理を両方で書いてみると、 文化の違いが見えます。
1 2 3 4 5 6 7 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) out = (df[df['SSDSE-B-2026']==2023] .assign(地域=lambda d: d['Prefecture'].map(region_map)) .groupby('地域')['A1101'].sum() .sort_values(ascending=False)) print(out) |
1 2 3 4 5 6 7 8 | import polars as pl df = pl.read_csv('data/raw/SSDSE-B-2026.csv', skip_rows_after_skiprows=[1]) out = (df.filter(pl.col('SSDSE-B-2026')==2023) .with_columns(pl.col('Prefecture').map_elements(lambda x: region_map[x]).alias('地域')) .group_by('地域') .agg(pl.col('A1101').sum()) .sort('A1101', descending=True)) print(out) |
polars は式 (Expression) APIが中心で、 並列実行・最適化(query planner)が組み込まれている。 SSDSE-B 程度では差は小さいが、 1億行を超えると 10〜30 倍の差が出る。
分析前に DataFrame の「健康診断」をする習慣を持つと、 後段のバグを激減できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | def health_check(df: pd.DataFrame, name: str = 'df'): print(f'=== {name} の健康診断 ===') print(f'shape : {df.shape}') print(f'memory : {df.memory_usage(deep=True).sum()/1e6:.2f} MB') print(f'重複行 : {df.duplicated().sum()} 件') print(f'全列で欠損なし: {(df.isna().sum()==0).sum()} / {len(df.columns)} 列') print(f'カラム名重複 : {df.columns.duplicated().sum()} 件') print('--- dtype 分布 ---') print(df.dtypes.value_counts()) print('--- 欠損率 Top5 ---') print((df.isna().mean()*100).sort_values(ascending=False).head()) print('--- 数値列の describe ---') print(df.select_dtypes('number').describe().T[['mean','std','min','max']].head()) health_check(df23, 'SSDSE-B-2026 (2023)') |
この関数を common.py に置いておけば、 新しい DataFrame を扱うたびに health_check(df) 一発で全体像を把握できる。
1 2 3 4 5 6 7 | q1 = df23['A1101'].quantile(0.25) q3 = df23['A1101'].quantile(0.75) iqr = q3 - q1 lower, upper = q1 - 1.5*iqr, q3 + 1.5*iqr outliers = df23[(df23['A1101']<lower) | (df23['A1101']>upper)] print(outliers.index.tolist()) # → 北海道・埼玉・千葉・東京・神奈川・愛知・大阪・兵庫・福岡の9都道府県が上側外れ値として検出される |
1 2 3 4 5 6 7 8 | from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans features = ['A1101','高齢化率','A4101','L3221'] X = df23[features].dropna() Xs = StandardScaler().fit_transform(X) df23.loc[X.index,'cluster'] = KMeans(n_clusters=4, random_state=42, n_init='auto').fit_predict(Xs) print(df23.groupby('cluster')[features].mean()) |
1 2 3 | import seaborn as sns sns.boxplot(data=df23.reset_index(), x='地域', y='高齢化率', order=['北海道','東北','関東','中部','近畿','中国','四国','九州']) plt.xticks(rotation=30); plt.tight_layout() |
1 2 3 4 5 6 7 8 | # 通常 res = df['A1303'] / df['A1101'] * 100 # eval版は数倍高速になる(大規模時) res = df.eval('A1303 / A1101 * 100') # query もパース後にC実装 big_pref = df.query('A1101 > 1e7') |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | # ── ここから下の書き方見本で使うデータを用意します ── # SSDSE-B は 1 行目が英字コード(A1101 など)、2 行目が日本語名。 # 見本では英字コードを使うので、1 行目を見出しにして読み込みます。 import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=0) df = df[df['Code'].astype(str).str.match(r'^R\d{5}$', na=False)].copy() for _c in ['A1101', 'A1303', 'SSDSE-B-2026']: df[_c] = pd.to_numeric(df[_c], errors='coerce') # 見本の中で使う派生列(地域ブロック・高齢化率)と、結合用のもう 1 枚 _region = {'北海道':'北海道','青森県':'東北','岩手県':'東北','宮城県':'東北','秋田県':'東北', '山形県':'東北','福島県':'東北','茨城県':'関東','栃木県':'関東','群馬県':'関東', '埼玉県':'関東','千葉県':'関東','東京都':'関東','神奈川県':'関東'} df['地域'] = df['Prefecture'].map(_region).fillna('その他') df['高齢化率'] = df['A1303'] / df['A1101'] * 100 df2 = df[['Prefecture', 'A1101']].copy() summary = pd.DataFrame() for chunk in pd.read_csv('huge.csv', chunksize=100_000, encoding='utf-8'): chunk['年度'] = chunk['日付'].str[:4] summary = pd.concat([summary, chunk.groupby('年度')['金額'].sum()], axis=0) print(summary.groupby(level=0).sum()) |
| やりたいこと | 書き方 |
|---|---|
| CSV 読み込み | pd.read_csv(path, encoding='utf-8') |
| 先頭5行 | df.head() |
| shape / dtypes | df.shape, df.dtypes |
| 記述統計 | df.describe() |
| 条件抽出 | df[df['x']>0] / df.query('x>0') |
| 列選択 | df[['a','b']] |
| 行参照 | df.loc[label] / df.iloc[pos] |
| 並べ替え | df.sort_values('x', ascending=False) |
| 欠損 | df.isna().sum() / df.dropna() |
| 集約 | df.groupby('k')['v'].sum() |
| 結合 | df1.merge(df2, on='k') |
| ピボット | df.pivot_table(index, columns, values) |
| 時系列リサンプル | df.resample('M').mean() |
| プロット | df.plot(kind='bar') |
典型的な5操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | # ── ここから下の書き方見本で使うデータを用意します ── # SSDSE-B は 1 行目が英字コード(A1101 など)、2 行目が日本語名。 # 見本では英字コードを使うので、1 行目を見出しにして読み込みます。 import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=0) df = df[df['Code'].astype(str).str.match(r'^R\d{5}$', na=False)].copy() for _c in ['A1101', 'A1303', 'SSDSE-B-2026']: df[_c] = pd.to_numeric(df[_c], errors='coerce') # 見本の中で使う派生列(地域ブロック・高齢化率)と、結合用のもう 1 枚 _region = {'北海道':'北海道','青森県':'東北','岩手県':'東北','宮城県':'東北','秋田県':'東北', '山形県':'東北','福島県':'東北','茨城県':'関東','栃木県':'関東','群馬県':'関東', '埼玉県':'関東','千葉県':'関東','東京都':'関東','神奈川県':'関東'} df['地域'] = df['Prefecture'].map(_region).fillna('その他') df['高齢化率'] = df['A1303'] / df['A1101'] * 100 df2 = df[['Prefecture', 'A1101']].copy() df.head() # 先頭5行 df['A1303'].mean() # 列の平均(65歳以上人口 A1303) df[df['A1303'] > 5e5] # フィルタ df.groupby('地域')['A1101'].sum() # 集計(地域は派生列) df.merge(df2, on='Prefecture') # 結合 |
data/raw/SSDSE-B-2026.csv(CSV、 cp932、 1行目=英字コード列名(A1101 等)/2行目=日本語列名)。 各行=年度×都道府県、 列=人口・経済・教育などの統計値。1 2 3 4 5 6 7 8 9 10 11 12 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) print('shape =', df.shape) # (564, 112) 程度: 47県 × 12年 print('dtypes head:'); print(df.dtypes.head(8)) print('describe (数値列):'); print(df.describe().T.head(5)) # 2023年だけ抜き出して都道府県をインデックス化 df23 = df[df['SSDSE-B-2026']==2023].set_index('Prefecture') print(df23.shape) # (47, 111) print(df23['A1101'].nlargest(5)) # 東京・神奈川・大阪・愛知・埼玉 print('全国合計 =', df23['A1101'].sum()) # 約1.24億 |
skiprows=[1] は SSDSE 特有の「2行目の日本語列名」を読み飛ばし、 1行目の英字コード列名(A1101 等)をそのまま列名として使うため。 set_index で「Prefecture」を主キーに昇格させると df23.loc['東京都'] で1行を直接取れる。 これが DataFrame の「インデックス」の威力。SSDSE-B-2026 (2023年) を実際に計算すると:
| 順位 | 都道府県 | 総人口 | 65歳以上人口 | 高齢化率(%) |
|---|---|---|---|---|
| 1 | 秋田県 | 約 91.4万 | 約 35.7万 | 39.1 |
| 2 | 高知県 | 約 66.6万 | 約 24.2万 | 36.3 |
| 3 | 徳島県 | 約 69.5万 | 約 24.6万 | 35.4 |
この計算は df23.assign(rate=df23['A1303']/df23['A1101']*100).nlargest(3,'rate') の1行で出る。 これが DataFrame の表現力です。
| 業界 | DataFrame の役割 | 代表ツール / 規模 |
|---|---|---|
| 金融(クオンツ) | 日次・分次の価格データを時系列 DataFrame で保持し、 移動平均・ボラティリティを列演算。 バックテスト基盤の中核。 | pandas + Arrow / 数億行 |
| EC・小売 | 購買ログを groupby('user_id') で集計し RFM 分析やコホート分析。 在庫最適化・需要予測の前処理。 | pandas / polars / 数千万行 |
| 製造業(IoT) | センサーログをミリ秒単位の DataFrame に整形し、 異常検知・予知保全モデルへ。 リサンプリングが頻出。 | pandas + Dask / 数十億行 |
| 医療(電子カルテ) | 患者ID×検査項目×時刻の DataFrame で疫学解析。 欠損処理・標準化が中心課題。 | pandas + R data.frame |
| 広告テック | 入札ログを Spark DataFrame で分散処理し、 CTR / CVR 予測の特徴量テーブルを生成。 | PySpark / TB 級 |
| 公的統計(本コンペ) | SSDSE-B のような47都道府県データを DataFrame で読み込み、 EDA・回帰・可視化の起点に。 | pandas / 数千〜数万行 |
| ツール | 速度 | メモリ | 学習コスト | 分散 | 推奨規模 |
|---|---|---|---|---|---|
| pandas | 中 | ×(メモリ載せ) | 低(標準) | × | 〜1GB |
| polars | 高(5-30倍) | ○ Arrow | 中(式 API) | 部分 | 〜100GB |
| Dask | 中 | ○(パーティション) | 低(pandas互換) | ○ | 〜TB |
| PySpark | 高 | ○ | 高 | ○ | TB〜PB |
| cuDF (RAPIDS) | 超高(GPU) | △ (GPU VRAM) | 低(pandas互換) | × | 〜数十GB |
| R data.frame | 中(dplyr) | × | 中 | × | 〜1GB |
SSDSE-B(数千行)の規模なら pandas で十分。 1億行を超えたら polars または Dask を検討。
df['A1101'].agg(['mean','median','std']))map → groupby)merge(..., suffixes=('_13','_23')))あるアナリストが df_sub = df[df['年度']==2023] としてから df_sub['人口'] = df_sub['人口']/1000 と代入。 警告は出たが無視。 後で元の df を見ると、 値が一部だけ書き換わっており、 報告書の数値が再現できなくなった。 対策: .copy() を明示し、 警告は即座に直す習慣を。
SSDSE 系データを pd.read_csv で読むと、 郵便番号「0600000」が 600000 と読み込まれてゼロが消失。 ジオコーディングで使えなくなる。 対策: dtype={'郵便番号': str} を明示。
100万行の DataFrame に対して for i in range(len(df)): df.loc[i,'new'] = f(df.loc[i,'x']) と書いて 30 分待った末にメモリ不足で落ちる。 対策: df['new'] = df['x'].apply(f) または完全な vectorize で 1 秒以下に。
地域名「北海道 」(末尾スペース)と「北海道」が別グループに。 集計結果が2行に分裂し、 グラフの色が変。 対策: df['地域'] = df['地域'].str.strip() を前処理で徹底。
欠損のある列を fillna(0).mean() で計算したため、 平均値が本来より低く出た。 対策: 欠損は欠損として扱い (skipna=True がデフォ)、 「補完するべきか除外するべきか」を意識的に選ぶ。
SSDSE-B-2026 (2023年) の 5 都道府県を抜粋し、 地域ブロック別の総人口 (A1101) 平均と県数を groupby で計算する。
| 都道府県 | 地域 | A1101 総人口 [万人] |
|---|---|---|
| 北海道 | 北海道・東北 | 509 |
| 秋田県 | 北海道・東北 | 91 |
| 東京都 | 関東 | 1409 |
| 神奈川県 | 関東 | 923 |
| 埼玉県 | 関東 | 733 |
1 2 3 4 5 6 7 8 | import pandas as pd df = pd.DataFrame({ '都道府県':['北海道','秋田県','東京都','神奈川県','埼玉県'], '地域':['北海道・東北','北海道・東北','関東','関東','関東'], 'A1101':[509,91,1409,923,733], }) g = df.groupby('地域')['A1101'].agg(['mean','count']) print(g) |
💬 手計算 (Step 2) の 300.0 / 1021.7 と Python の groupby 出力が一致(小数の表示桁数のみ異なる)。 SSDSE-B-2026 の地域別集計の基本パターン。
最小限のスニペットで動作確認できる例。 公的データ(SSDSE 等)を想定しています。
1 2 3 4 5 6 7 8 9 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df23 = df[df['SSDSE-B-2026']==2023].set_index('Prefecture') print(df23.shape) # (47, 111) df23['高齢化率'] = df23['A1303'] / df23['A1101'] * 100 # 高齢化率が高い順 Top 5 → 秋田39.1・高知36.3・徳島35.4・山口35.4・青森35.2 print(df23['高齢化率'].nlargest(5).round(1)) |
df[df.A > 0]['B'] = 1 のように 2 段階で書くと、 pandas は元データのコピーに書き込むことがあり、 変更が消える。 警告は「意図どおりか確認せよ」という合図。 .loc[条件, 列] でまとめて指定するか、 明示的に .copy() を取る。inplace=True は返り値が None になるためメソッドを繋げられず、 途中結果も残らないので手戻りが効かない。 内部でコピーを作る実装も多く、 期待するほど速くもない。 新しい変数に代入する書き方のほうが、 読みやすく戻しやすい。dtype=str を明示して読む。
┌────────────────┐
│ プログラミング言語 │
│ (Python / R) │
└────────┬────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ NumPy │ │ pandas │ │ polars │
│ (数値配列) │ │ (DataFrame) │ │ (Rust製) │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└─────┬───────┴─────┬───────┘
▼ ▼
┌──────────────────────────┐
│ 表形式データの世界共通モデル │
│ ── DataFrame ── │
└────────────┬──────────────────┘
│
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 入力 │ │ 変形 │ │ 出力 │
│ CSV/JSON│ │ merge │ │ グラフ │
│ API/SQL │ │ groupby │ │ モデル │
│ Parquet │ │ pivot │ │ CSV │
└─────────┘ └─────────┘ └─────────┘
DataFrame は「入力 → 変形 → 出力」の中継地点。 ここを自由に操れることが、 現代データサイエンスの基礎体力です。
これまでのレシピをつなげて、 「データ取得 → 健康診断 → 派生列 → 集約 → モデル前処理 → 可視化 → 保存」の一連のパイプラインを書いてみます。 関数化しておけば再現実行が楽になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | # ── ここから下の書き方見本で使うデータを用意します ── # SSDSE-B は 1 行目が英字コード(A1101 など)、2 行目が日本語名。 # 見本では英字コードを使うので、1 行目を見出しにして読み込みます。 import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=0) df = df[df['Code'].astype(str).str.match(r'^R\d{5}$', na=False)].copy() for _c in ['A1101', 'A1303', 'SSDSE-B-2026']: df[_c] = pd.to_numeric(df[_c], errors='coerce') # 見本の中で使う派生列(地域ブロック・高齢化率)と、結合用のもう 1 枚 _region = {'北海道':'北海道','青森県':'東北','岩手県':'東北','宮城県':'東北','秋田県':'東北', '山形県':'東北','福島県':'東北','茨城県':'関東','栃木県':'関東','群馬県':'関東', '埼玉県':'関東','千葉県':'関東','東京都':'関東','神奈川県':'関東'} df['地域'] = df['Prefecture'].map(_region).fillna('その他') df['高齢化率'] = df['A1303'] / df['A1101'] * 100 df2 = df[['Prefecture', 'A1101']].copy() import os import pandas as pd import matplotlib.pyplot as plt from sklearn.preprocessing import StandardScaler REGION_MAP = { '北海道':'北海道','青森県':'東北','岩手県':'東北','宮城県':'東北','秋田県':'東北','山形県':'東北','福島県':'東北', '茨城県':'関東','栃木県':'関東','群馬県':'関東','埼玉県':'関東','千葉県':'関東','東京都':'関東','神奈川県':'関東', '新潟県':'中部','富山県':'中部','石川県':'中部','福井県':'中部','山梨県':'中部','長野県':'中部','岐阜県':'中部','静岡県':'中部','愛知県':'中部', '三重県':'近畿','滋賀県':'近畿','京都府':'近畿','大阪府':'近畿','兵庫県':'近畿','奈良県':'近畿','和歌山県':'近畿', '鳥取県':'中国','島根県':'中国','岡山県':'中国','広島県':'中国','山口県':'中国', '徳島県':'四国','香川県':'四国','愛媛県':'四国','高知県':'四国', '福岡県':'九州','佐賀県':'九州','長崎県':'九州','熊本県':'九州','大分県':'九州','宮崎県':'九州','鹿児島県':'九州','沖縄県':'九州', } def load(path='data/raw/SSDSE-B-2026.csv'): return pd.read_csv(path, encoding='cp932', skiprows=[1]) def select_year(df, year=2023): return df[df['SSDSE-B-2026']==year].set_index('Prefecture') def enrich(df): return df.assign( 高齢化率 = lambda d: d['A1303']/d['A1101']*100, 生産年齢比 = lambda d: d['A1302']/d['A1101']*100, 地域 = lambda d: d.index.to_series().map(REGION_MAP), ) def aggregate_by_region(df): return df.groupby('地域').agg( 人口=('A1101','sum'), 平均高齢化率=('高齢化率','mean'), 県数=('A1101','count'), # 集計後の列名は 人口/平均高齢化率/県数。'A1101' はもう存在しない ).sort_values('人口', ascending=False) def standardize(df, cols): sub = df[cols].dropna() return pd.DataFrame(StandardScaler().fit_transform(sub), index=sub.index, columns=cols) def main(): df = load() df23 = select_year(df, 2023) df23 = enrich(df23) print(aggregate_by_region(df23)) feat = ['A1101','高齢化率','A4101'] Z = standardize(df23, feat) os.makedirs('outputs', exist_ok=True) # 保存先が無いと to_csv は失敗する Z.to_csv('outputs/standardized_2023.csv') print('saved: outputs/standardized_2023.csv') if __name__ == '__main__': main() |
この骨格は、 SSDSE-A / SSDSE-C / e-Stat の他のデータセットにも流用できます。 「読み込み・抽出・加工・集約・標準化・保存」を独立関数にしておくのが鍵。
read_csv / head / describe / plot で慣れるloc / iloc / query / assign でメソッドチェーンを習得groupby / agg / transform / merge で集約と結合pivot_table / melt / resample で形を変えるdtype / category / chunksize でメモリ最適化pd.testing / pytest でテスト駆動な分析を開始DataFrame は奥が深いですが、 上記のロードマップで2ヶ月もあれば「自分の道具」と呼べるレベルに達します。 SSDSE-B-2026 を題材に、 毎週新しい技を1つずつ追加していくのが効率的な学び方です。
| dtype | 1要素あたりサイズ | 100万行のメモリ | 用途 |
|---|---|---|---|
| int8 | 1 byte | 1 MB | 小さい整数(年齢など) |
| int32 | 4 byte | 4 MB | 通常の整数(20億まで) |
| int64 | 8 byte | 8 MB | pandas デフォルト |
| float32 | 4 byte | 4 MB | ML 用(精度7桁) |
| float64 | 8 byte | 8 MB | pandas デフォルト |
| object (str) | ~50 byte | 50 MB | 汎用文字列 |
| string[pyarrow] | ~3 byte | 3 MB | 短い文字列(Arrow) |
| category | ~1 byte+辞書 | 1 MB前後 | 少数カテゴリ(地域名) |
| datetime64[ns] | 8 byte | 8 MB | 日時 |
| bool | 1 byte | 1 MB | フラグ列 |
教訓:100万行を超える DataFrame はdtype の見直しだけで 10〜100 倍のメモリ削減が可能。 まず df.info(memory_usage='deep') で現状把握を。
df.describe() は基本ですが、 統計分析では scipy.stats や statsmodels と組み合わせると強力です:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | # ── ここから下の書き方見本で使うデータを用意します ── # SSDSE-B は 1 行目が英字コード(A1101 など)、2 行目が日本語名。 # 見本では英字コードを使うので、1 行目を見出しにして読み込みます。 import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=0) df = df[df['Code'].astype(str).str.match(r'^R\d{5}$', na=False)].copy() # 1 行目を見出しにすると数値の列も文字列で入ってくるので、まとめて数値に直す for _c in df.columns: if _c not in ('Code', 'Prefecture'): df[_c] = pd.to_numeric(df[_c], errors='coerce') # 見本の中で使う派生列(地域ブロック・高齢化率)と、結合用のもう 1 枚 _region = {'北海道':'北海道','青森県':'東北','岩手県':'東北','宮城県':'東北','秋田県':'東北', '山形県':'東北','福島県':'東北','茨城県':'関東','栃木県':'関東','群馬県':'関東', '埼玉県':'関東','千葉県':'関東','東京都':'関東','神奈川県':'関東'} df['地域'] = df['Prefecture'].map(_region).fillna('その他') df['高齢化率'] = df['A1303'] / df['A1101'] * 100 df2 = df[['Prefecture', 'A1101']].copy() # この見本で使う 2023 年度・47 都道府県の表(都道府県名を索引にする) df23 = (df[df['SSDSE-B-2026'] == 2023] .set_index('Prefecture') .drop(columns=['SSDSE-B-2026', 'Code'], errors='ignore')) from scipy import stats import statsmodels.api as sm # 47都道府県の高齢化率と消費支出(L3221)の相関と検定 r, p = stats.pearsonr(df23['高齢化率'], df23['L3221']) print(f'相関係数 r = {r:.3f}, p値 = {p:.4f}') # 単回帰モデル X = sm.add_constant(df23['高齢化率']) y = df23['L3221'] model = sm.OLS(y, X).fit() print(model.summary().tables[1]) |
DataFrame は scipy / statsmodels / sklearn とシームレスに連携。 列を変えるだけで様々な分析を試せる柔軟性が魅力。
DataFrame はライブラリの一機能ではなく、 「データを語る言語」です。 SQL 話者は WHERE/GROUP BY/JOIN で会話し、 R ユーザーは dplyr の動詞(filter/mutate/group_by/summarise)で会話し、 pandas ユーザーは query/assign/groupby/agg/merge で会話します。 表現は違えど、 背後にある関係代数の文法は同じ。 一度身につければ、 ツールが変わっても本質は使い回せます。
SSDSE-B-2026 のような実データを通して練習を重ねれば、 「都道府県データを開いて → 集めて → 結んで → 描く」までを 5 分で書けるようになります。 そこから先は、 問いを立てる力と結果を解釈する力の世界です。 道具としての DataFrame を超えて、 「データで物事を考える」ステージへ進むための土台がここにあります。
df.eval() と df.query() を使って、 「2023年 ∧ 人口 > 200万 ∧ 高齢化率 < 30」 の県を 1 行で抽出するコードを書け。category 型にしてメモリ使用量がいくつになるか、 memory_usage(deep=True) で比較せよ。ヒント: これらの演習で「DataFrame を呼吸のように扱う」感覚が身につけば、 統計データ解析コンペでも自信を持って臨めるはずです。
DataFrame と Excel シートは似ているが本質的に違う。 Excel は「セル参照と数式の二重構造」だが、 DataFrame は「列が型を持つ純粋な配列」。 シート間参照のような関数チェーンはなく、 全ての計算は明示的なコードで書く。 これがバージョン管理と再現性の鍵。
大規模 DataFrame は Parquet で保存すると、 CSV と比べてサイズ 1/5・読み込み速度 10 倍。 列志向のため不要な列を読まずに済むのが効く。 SSDSE-B 程度ならどちらでも良いが、 自前ログを保存するなら Parquet 一択。
マルチプロセス化したいときは swifter や modin、 GPU を使うなら cuDF、 究極のスケールは Dask や PySpark。 ただし、 まず pandas を完全に使いこなすのが先決。 多くの「pandas は遅い」 と言われる場面は、 dtype 最適化と vectorize で 9 割は解決する。
DataFrame は「行・列・型・インデックス」の四重構造で、Excel シートとも NumPy 配列とも違う独自の存在である。ここでは SSDSE-B-2026 の都道府県データを題材に、DataFrame の振る舞いを「相関」「ヒストグラム」「箱ひげ」 の三方向から可視化し、表として読み解くことで理解を完成させる。 DataFrame という抽象データ型がなぜデータサイエンスの共通言語になったのかを、 実値で示す。
下図は df.corr() で得られる相関行列を可視化したもの。 DataFrame は「数値列の集合」として相関を直接計算できる。 これが配列ではなく DataFrame である最大のメリットの一つ。 列名を保持したまま、計算結果も DataFrame で返るため、 そのまま seaborn.heatmap() に渡せる。

→ 総人口(A1101)と出生数(A4101)・65歳以上人口(A1303)の強い正の相関が一目で読める(人口規模の大きい県ほど出生数も高齢者数も大きい)。 DataFrame の列名がそのまま軸ラベルに反映されるため、 配列ベースの実装より圧倒的に短いコードで等価な可視化が完成する。
df.hist() 一行で「すべての数値列」のヒストグラムが描ける。 DataFrame の「列に型がついた」設計のおかげで、 文字列列は自動で除外される。 配列だったら手作業で数値列を抜き出して loop する必要があった部分が、 DataFrame では完全に自動化されている。

→ 人口は右に長く裾を引き (東京・神奈川・大阪)、 高齢化率は左右対称に近い正規分布形。 DataFrame は列の dtype を知っているからこそ、「数値列のみヒストグラム」 という処理が自動で実現できる。
df.boxplot() で複数列を並べて分布形状を比較できる。 列ごとに別 unit でも、 内部では DataFrame が「列名 → series → 配列」 の階層を順に展開してくれる。 これも DataFrame の四重構造があるからこそ、 1 行で完結する処理である。

→ 人口は東京・神奈川・大阪が外れ値として浮き上がる。 DataFrame は外れ値検出後も、 同じインデックスで他の列にアクセスできるため、 「外れ値の都道府県名は?」 という質問にすぐ答えられる。
DataFrame は数学的には次のように書ける:
$$ D = \{ (i, c, t_c, v_{i,c}) \mid i \in I,\ c \in C,\ t_c \in T \} $$
この四重組 $(i, c, t_c, v_{i,c})$ が DataFrame の正体である。 NumPy 配列は $(i, j, v_{i,j})$ の三重組しか持たない。 「列に名前」「列に型」 という 2 つの追加情報が、 DataFrame を「データサイエンスの共通言語」 にしている本質。
| 特徴 | DataFrame | NumPy 配列 | Excel シート | CSV |
|---|---|---|---|---|
| 列ラベル | ○ (文字列) | × | △ (1 行目) | △ (1 行目) |
| 列単位の型 | ○ (dtype) | × (全要素同型) | △ (セル単位) | × (全部文字列) |
| 欠損 (NaN) 概念 | ○ ファーストクラス | △ (float のみ) | ○ 空セル | △ 空文字列 |
| インデックス (行ラベル) | ○ 任意のラベル | △ (整数のみ) | △ (行番号) | × |
| バージョン管理 | ○ (コード) | ○ (コード) | × (バイナリ) | ○ (テキスト) |
| groupby/集計 | ○ 1 行 | × 手書きループ | △ ピボット | × 不可 |
| 大規模対応 | △ (10⁷ 行) | ○ (高速) | × (10⁶ 行限界) | ○ (無制限) |
→ 「列ラベル + 列単位の型 + 欠損ファーストクラス + groupby」 の組み合わせが、 DataFrame を独占的地位に押し上げている要素。 Excel から DataFrame に乗り換える理由は、 ほぼこの四点に集約される。
このコードでやること: SSDSE-B-2026 の都道府県データを読み込み、 DataFrame の四重構造 (インデックス・列名・型・値) を一つずつ確認する。 各属性が DataFrame の本質を構成していることを実値で示す。
📥 入力データ (SSDSE-B-2026 抜粋):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', skiprows=[1], encoding='cp932') # 1. インデックス I print("Index (行ラベル):", df.index[:5].tolist()) # 2. 列名 C print("Columns (列ラベル):", df.columns[:5].tolist()) # 3. 列ごとの型 t_c print("dtypes (列ごとの型):") print(df.dtypes.head()) # 4. 値 v_{i,c} print("値 v[0, 'A1101'] =", df.loc[0, 'A1101']) print("Shape (行数, 列数):", df.shape) |
📤 実行例 (実際の出力):
💬 これが DataFrame の四重構造である。 インデックス (0-563)、 列名 (112 列)、 dtype (int64/object が混在)、 値 (5092000)。 これら 4 つが揃って初めて「DataFrame」 と呼べる。 NumPy 配列だったら 112 列が全部同じ型になり、 都道府県名 (Prefecture) のような文字列列は持てなかった。
df[df['x']>0]['y'] = 0 は警告が出る。 必ず .loc[mask, 'y'] = 0 を使う。 copy/view の区別が DataFrame の最大の落とし穴。Int64 (大文字) を指定。merge() の片方に重複キーがあると行数が爆発する。 マージ前後で len(df) を必ず比較する。df['a']['b'] = 1 は無視される (copy に書き込んでいる)。 .loc[ を必ず使う。sort=False。df[df['x']>0]['y'] = 0 がうまく動かない理由は? (答: chained indexing で copy に書き込まれる)df.merge(other, on='key') 後に行数が増える原因は? (答: 結合キーに重複がある)→ 5 問のうち 4 問以上正解できれば DataFrame の構造を理解している。 特に問 3 の copy/view は実務で最も多いバグ源。
DataFrame をマスターしたら次は: pandas (DataFrame を提供する Python ライブラリ) / CSV (DataFrame のテキスト表現) / データクレンジング / 集計 / データ結合 / フィルタリング / ピボットテーブル / 縦持ち・横持ち / 欠損メカニズム / tidy data へ。
pandas の DataFrame は内部で「BlockManager」 と呼ばれる構造を持つ。 同じ dtype の列は 1 つの NumPy 2D 配列にまとめられ (Block)、 BlockManager がそれら Block を管理する。 これにより、 同 dtype 列の演算は NumPy の高速 SIMD で処理できる。 一方で、 異 dtype を混在させると Block が分断され、 効率が落ちる。 SSDSE-B-2026 で 110 列を扱うとき、 数値列と文字列列を分けて操作するのが性能の鉄則。
メモリ最適化の具体策: (1) df.memory_usage(deep=True) で各列のメモリ消費を可視化、 (2) 小さい整数は int8/int16 に変換 (110 行 × 47 行なら int32 で十分)、 (3) 繰り返し文字列は category dtype に変換 (例: 都道府県名は 47 種類のみで、 メモリが 1/10 になる)、 (4) 日付列は datetime64 に変換し object 型を避ける。 SSDSE-B-2026 全体で、 これらの最適化を適用するとメモリ消費が 60-80% 減らせる。
大規模化への道筋: SSDSE-B-2026 程度の規模 (47 行 × 110 列、 数 MB) なら pandas で問題ないが、 数十 GB を扱うようになると Dask (チャンク並列)、 polars (Rust 製の高速 DataFrame)、 cuDF (GPU 版) への移行を検討する。 ただし、 まずは pandas の最適化を尽くしてから検討するのが現実的。 多くのケースで「pandas は遅い」 と言われる場面は、 dtype 最適化と vectorize で 9 割は解決する。
DataFrame という抽象データ型は、 1996 年の R 言語の data.frame が起源。 2008 年に Wes McKinney が pandas として Python に移植し、 2010 年代に「データサイエンスの共通言語」 となった。 Python 以外では、 Julia の DataFrames.jl、 Scala の Spark DataFrame、 Rust の Polars DataFrame、 JavaScript の Danfo.js など、 各言語に DataFrame が移植されている。 これは「列 + 型 + ラベル」 という抽象が、 データ分析の本質的な構造を捉えているからに他ならない。
Arrow という共通バイナリフォーマットの登場で、 言語間のデータ共有が高速化した。 SSDSE-B-2026 を pandas で読み込み、 Arrow に変換し、 Rust の Polars で処理し、 R の data.frame に戻す — こうした多言語パイプラインが現実的になっている。 DataFrame は単なるデータ型を超えて、 データサイエンスのインフラそのものになっている。
DataFrame の真価は「複数の操作を連鎖させて洞察を引き出す」 ところにある。 SSDSE-B-2026 を題材に、 1 つの DataFrame から 5 つの異なる分析を導く例を示す。 すべて pandas の chainable な API (メソッドチェーン) で実現できる。
df.groupby('地域')['A4101'].sum() で 8 地域 (北海道、 東北、 関東、 中部、 近畿、 中国、 四国、 九州沖縄) ごとに集計。 人口の多い関東が圧倒的に高いことが分かる。df.assign(birth_rate=df['A4101']/df['A1101']*1000) で人口千人あたり出生数の新列を作り、 ランキング。 沖縄が上位、 東北の各県が下位という構造が見える。df[['A1101','A1303','A4101']].corr() で 3 変量 (総人口・65歳以上人口・出生数) の相関行列を計算。 いずれも人口規模に比例し、 強い正相関が連鎖していることを発見。df[df['A1101'] > df['A1101'].quantile(0.95)] で総人口の上位 5% を抽出。 東京・神奈川がここに入る。pd.merge(a, b, on='Prefecture') で結合し、 「経年変化と現状」 を同時に分析。これら 5 つの分析は、 SQL では各々別クエリ、 Excel では各々別シートが必要だが、 DataFrame では 1 つのオブジェクトから連続的に導ける。 これが「データ分析の共通言語」 たる所以。
DataFrame を実務で使うときに守るべきベストプラクティス。
df.copy() でビューと値を意識して使い分けているか? (SettingWithCopyWarning を防ぐ).loc[] / .iloc[] を使ってインデックスアクセスを明示しているか?df.dtypes を必ず確認してからスタートしているか?category dtype, Int8/16) を適用しているか?df.merge() 後に len(df) を比較して行数爆発を検知しているか?df['a']['b']=1) を絶対に使っていないか?sort=False オプションで元順序を保っているか?→ このチェックリストの 8 項目を満たせば、 SSDSE-B-2026 程度のデータなら問題なく扱える。 数十 GB を超えるなら polars や Dask への移行検討が次のステップ。
Q1: DataFrame と pandas.Series の違いは?
Series は 1 次元 (列単位)、 DataFrame は 2 次元 (表形式)。 df['A1101'] は Series を返し、 df[['A1101', 'A1303']] は DataFrame を返す。 Series もインデックスと dtype を持つ点で NumPy 配列と異なる。 DataFrame は内部的に「列 = Series の辞書」 と理解すると分かりやすい。
Q2: pandas と polars はどちらを使うべき?
SSDSE-B-2026 程度 (数 MB) なら pandas で十分。 GB 級のデータや、 chainable API の美しさを求めるなら polars。 polars は Rust 製で 5-10 倍速いが、 エコシステム (sklearn 連携など) は pandas が圧倒的。 まずは pandas を完全に使いこなしてから polars を検討するのが現実的。
Q3: CSV と Parquet どちらで保存すべき?
公開・人間可読が重要なら CSV、 速度・容量が重要なら Parquet。 SSDSE-B-2026 のような公開データセットは CSV が標準だが、 自前ログを保存するなら Parquet 一択。 サイズは 1/5、 読み込みは 10 倍速い。 列指向 (columnar) なので、 一部の列だけ読む処理 (典型的な分析) で特に有利。
Q4: groupby の後で何が返ってくる?
df.groupby('地域') は GroupBy オブジェクトを返す (まだ集計していない)。 続けて .sum()、 .mean()、 .agg([]) などで実際の集計を実行。 「遅延評価」 で計算量を最適化している。 GroupBy 自体は実体を持たないので、 デバッグ時は for k, v in grouped: print(k, v.head()) で中身を見る。
Q5: DataFrame を学ぶ最良の入門書は?
日本語なら『Python for Data Analysis 第 3 版』 (Wes McKinney) の日本語訳版。 著者は pandas の作者。 公式ドキュメントの "10 Minutes to pandas" は最速入門。 実践的な書籍なら『データ分析実務スキル検定 公式テキスト』 や『前処理大全』 (本橋智光) が SSDSE-B を扱う分析者に必須。
DataFrame 全体を一本の糸で貫くと、 次の 3 つの設計思想に集約される。
これらを意識して DataFrame を使えば、 単なる「Excel の代替」 ではなく「現代データ分析の中核装置」 として機能させられる。 SSDSE-B-2026 のような公的データセットを扱う際も、 これらの設計思想を意識することで、 短く、 美しく、 再現可能な分析コードが書ける。
DataFrame は単一のデータ型を超えて、 多様な世界を繋ぐ役割を果たしている。 以下に主要な拡張領域を列挙する。
fit(X, y) はそのまま DataFrame を受け取る。 列名が SHAP 値や特徴量重要度の表示に自動で使われ、 デバッグが楽になる。sns.scatterplot(data=df, x='A1101', y='A4101') 一行で SSDSE-B の散布図(総人口×出生数)ができる。pd.read_sql() でデータベースから DataFrame を直接読める。 SQLAlchemy 経由で MySQL/PostgreSQL/SQLite に書き戻すこともできる。pd.DataFrame(response.json()) で即 DataFrame 化。 外部データ取得からの一気通貫が可能。read_*() 関数で読める。 「形式に依存しないデータ取り込み」 が DataFrame の強み。pd.read_csv('s3://bucket/file.csv') のような書き方で、 ローカルとクラウドが透過的。これらの接続性こそが DataFrame の真価。 SSDSE-B-2026 を読み込み、 機械学習で予測し、 SHAP で説明し、 可視化し、 結果を Parquet で保存し、 ダッシュボードに表示する — このすべてが DataFrame を中心に流れるように繋がる。 単なるデータ型ではなく、 データサイエンスのエコシステムそのものを支えるインフラ、 それが DataFrame である。
DataFrame の操作は、 一見シンプルでも内部では複雑な処理が走っている。 主要な操作のパフォーマンス特性を整理する。
df['x']): O(1)。 列ラベルから NumPy 配列への参照を返すだけ。 高速。df[df['x']>0]): O(n)。 全行に対する真偽値配列を作る。 SSDSE-B-2026 程度なら瞬時。pd.merge()): O(n log n) または O(n+m)。 内部でハッシュ結合かソート結合を選択。 メモリも食う。df.groupby()): O(n)。 ハッシュテーブルでグループ分け、 集計関数を各グループに適用。 cython で最適化。df.apply()): O(n) だが、 Python の関数呼び出しオーバーヘッドで遅い。 可能ならベクトル化で書き直す。for i, row in df.iterrows()): 極めて遅い。 1 万行で数秒。 可能なら絶対に使わない。 vectorize で書き直す。pd.pivot_table()): O(n log n)。 内部で groupby + unstack。 大規模データでは memory 注意。df.sort_values()): O(n log n)。 timsort 採用、 安定ソート。SSDSE-B-2026 のような 47 行 × 110 列のデータでは、 どんな操作も瞬時に終わる。 しかし数百万行を扱うようになると、 これらの計算量の違いが分単位の差になる。 「ベクトル化と groupby」 が高速化の二大原則。 これを意識するだけで、 pandas コードの実行時間は 10-100 倍違ってくる。 大規模分析を扱うようになる日のために、 今のうちから「iterrows を書かない」 習慣をつけることをお勧めしたい。 また、 numexpr や bottleneck のインストールで、 一部の数値計算が自動高速化される (pandas が内部で利用)。 これらは pip install のみで導入できるので、 環境構築時に忘れず入れておきたい。 これらの組み合わせは、 統計コンペで分析時間を 1/10 に短縮する力を持つ。 速度が出れば試行錯誤の回数が増え、 結果として分析の質も上がる、 という好循環が生まれる。
「DataFrame」は単独で完結する手法ではなく、 隣接領域と連携することで真価を発揮する。 具体的には次の 3 方向と密接につながる:
DataFrame は Python データ分析の中心データ構造で、 全工程のハブとなる。
「データフレーム」を実際に使うとき、 何をどう選ぶかを順に判断する。 上から順に答えていくと、 使うべき手法と評価の仕方が決まる。
dtype=str で読む。pivot_table と melt で行き来できる。データフレームは「表を変数として扱う」道具。 型と持ち方(縦横)を最初に決めておくと、 後段の処理がほとんど自動的に決まる。
このページの他の節が「正しく使う方法」を扱うのに対し、ここではエラーを出さずに間違った結果を返す瞬間だけを集める。DataFrame の事故の多くは例外ではなく、警告すら出ないまま数字だけがズレる。数値はすべて data/raw/SSDSE-B-2026.csv(encoding='cp932', skiprows=[1]/2023年・47都道府県)の実測から算出した。
初学者は DataFrame を Excel のような「上から何行目」のマス目だと思う。しかし pandas の内部では、行はインデックスラベルで識別される辞書に近い。2 番目の行という概念は .iloc を明示したときだけ存在し、+ や - のような演算は常にラベルを突き合わせてから計算する。この「位置ではなくラベル」という一点を腹落ちさせるだけで、以下の落とし穴の大半は消える。実測で言えば、2023年の総人口 A1101 は47都道府県で合計 124,353,000 人、平均 2,645,808.5 人だが、平均を上回るのはわずか 12 県(東京14,086,000/神奈川9,229,000/…と大都市に偏る右裾の重い分布)。「平均=真ん中」という直感が崩れるのと同じ構図で、「行=位置」という直感も崩れる。
① インデックスアラインメント ── 引き算したら全部 NaN。 人口上位3県と、CSV 掲載順の先頭3県を「位置で引きたい」つもりで a - b と書くと、pandas はラベルを揃えてから引く。ラベルが1つも重ならないので結果は全行 NaNになる。
# a = 人口上位3県, b = 掲載順の先頭3県(実測 A1101) a = [東京都 14086000, 神奈川県 9229000, 大阪府 8763000] b = [北海道 5092000, 青森県 1184000, 岩手県 1163000] a - b # ラベルで整列 → 6県すべて NaN(岩手/大阪/…全部 NaN) a.values - b.values # 位置で引く → [8994000, 8045000, 7600000] ✅
位置で計算したいなら .values(NumPy 配列に落とす)か .reset_index(drop=True) でラベルを揃える。逆に「一部の県だけ NaN が出る」ときは、ラベルの綴りゆれ(東京 vs 東京都)を疑う。
② dtype の object 化 ── 数字の列に文字が1個混ざると列ごと計算不能。 秘匿記号(- や ***)を1セルでも入れると、int64 だった列は object に降格し、.sum() は TypeError、.mean() は黙って壊れる。
# 架空: 鳥取県のセルに秘匿記号 '-' が入っていたと仮定 s.dtype # object(int64 から降格) s.sum() # TypeError: 数値と文字は足せない pd.to_numeric(s, errors='coerce').mean() # → 2,691,652.2('-' を NaN 化して除外。実測の正しい平均 2,645,808.5 とズレる)
怖いのは、集計が通ってしまうケース。1県が欠けたことに気づかず平均が 2,645,808.5 → 2,691,652.2 と上振れする(小さい県が消えるため)。読み込み直後に df.dtypes を必ず確認し、object になっている数値列は pd.to_numeric(..., errors='coerce') で明示変換する。na_values=['-','***'] を read_csv に渡すのが根本対策。
③ SettingWithCopyWarning ── スライスへの代入は「効くこともある」から厄介。 d23 = df[df['SSDSE-B-2026']==2023] のように条件抽出した結果は元 DataFrame のビューかもしれないしコピーかもしれない。この d23 にさらに d23['new'] = ... と代入すると SettingWithCopyWarning が出て、書き込みが元に反映されたりされなかったりと挙動が非決定的になる。
d23 = df[df['SSDSE-B-2026']==2023] # ← ビュー?コピー? d23['rate'] = d23['A1303'] / d23['A1101'] # ⚠ SettingWithCopyWarning # 正解: 抽出した瞬間に .copy() で意図を宣言する d23 = df[df['SSDSE-B-2026']==2023].copy() d23['rate'] = d23['A1303'] / d23['A1101'] # 警告なし ✅
合言葉は「抽出したら即 .copy()」。派生列を作る前提でスライスするなら、迷わずコピーを取る。なお pandas 3.0 の Copy-on-Write ではこの曖昧さは解消される方向だが、既存コードの移植では今も最頻出の警告。
上の3つは「起きてから気づく」のでは遅い。パイプラインの各段でアサーションを挟むと、事故は例外に変わり、CI で止められる。
# 実測を不変条件(invariant)として固定する assert len(d23) == 47 # 都道府県数 assert d23['A1101'].dtype.kind in 'iu' # 整数のまま(object化検出) assert d23['A1101'].sum() == 124353000 # 合計人口が保存されている assert d23.index.is_unique # アラインメント事故の予防
さらに一歩進めるなら、行数・型・値域・一意性を宣言的に検査する pandera や pandas.testing.assert_frame_equal をテストに組み込む。DataFrame は「表」ではなく「型と制約を持つ契約」だと捉え直すと、静かな破損は早期に大声を上げてくれる。時系列で追うなら、東京都の A1101 は 2012年 13,234,000 → 2023年 14,086,000 と単調増加で不変条件を置きやすいが、この種の性質を assert に落とすのが要点。