見出し画像

Pandasを初心にかえって整理する


第1章 Pandasとは何か、なぜ使うのか

**Pandas(パンダス)**は、Python言語で利用できるオープンソースのデータ分析・データ操作ライブラリです。元々は金融データの分析ニーズから生まれたライブラリで、開発者のウェス・マッキニー(Wes McKinney)氏が2008年にAQRキャピタルマネジメント社で開発を開始し、2009年にオープンソースとして公開されました (pandas - Wikipedia)。Pandasを使うことで、表形式のデータや時系列データを効率よく操作・分析するための機能を簡単に利用できます (pandas - Wikipedia)。具体的には、**Series(シリーズ)DataFrame(データフレーム)**という高性能なデータ構造を提供し、これらを用いた柔軟なデータ操作や集計機能を備えています (pandas - Wikipedia) (pandas - Wikipedia)。PandasはNumPyを土台としており、多くの処理がC言語やCythonで実装されているため高速に動作します (pandas - Wikipedia)。そのため、純粋なPythonで同等の処理を行うよりもはるかに少ないコード量で、高速なデータ処理が可能です。

では、なぜPandasを使うのでしょうか?最大の理由は、データ分析や前処理の作業を格段に効率化できるからです。例えば以下のような場合、Pandasは強力な助けになります。

  • Excelで行っていた作業の自動化: Excelは手軽な分析に適したツールですが、毎日更新されるデータに対して同じ集計を繰り返したい場合や、多量のデータを扱う場合は手作業では非効率です。Pandasを使えば、コードによる処理の自動化が可能となり、日々変化するデータに対してもワンクリック(またはコマンド一つ)で最新の分析結果を得られます (今さら聞けないPython - pandasを用いたデータ分析 #Databricks - Qiita)。これは、毎日新しいデータに対して分析を再実行したい場合定期レポートを自動生成したい場合に有用です (今さら聞けないPython - pandasを用いたデータ分析 #Databricks - Qiita)。

  • 再利用性と共有: Pandasで記述したコード(スクリプト)は再利用が容易で、バージョン管理もできます。同僚と分析手順そのものを共有したり、一緒に開発したりすることも簡単です (今さら聞けないPython - pandasを用いたデータ分析 #Databricks - Qiita)。Excelのブックを共有する場合、手順が属人的になりがちですが、コード化された処理は誰が実行しても同じ結果が得られ、分析プロセスの標準化にもつながります。

  • 高度で堅牢な分析: ビジネス上の重要な意思決定に関わる分析では、再現性や正確性が求められます。Pandasを使えば、複雑なデータ変換や集計も一貫した方法で実行でき、ヒューマンエラーを減らせます (今さら聞けないPython - pandasを用いたデータ分析 #Databricks - Qiita)。また、統計解析や機械学習の前処理といったより専門的で高度な分析にも耐えうる柔軟性があります。

  • 大量データの処理: Excelでは扱いに限界がある数百万行規模のデータでも、Pandasであれば適切な手法で処理可能です。Pandas自体はメモリに乗る範囲のデータを扱いますが、必要に応じてデータを分割して読み込む機能や、後述する他ライブラリとの連携によりビッグデータの一部を分析することもできます。コードによるアプローチは、データ量が増えてもスケールしやすいという利点があります。

  • 機械学習へのスムーズな移行: Pandasは機械学習エンジニアやデータサイエンティストにとって日常的なツールです (機械学習チュートリアル① - はじめに 〜 pandas入門)。機械学習パイプラインの中で、データの前処理(クリーニングや特徴量エンジニアリング)の段階でPandasが活躍します (機械学習チュートリアル① - はじめに 〜 pandas入門)。Pandasで整形・加工したデータをそのまま機械学習ライブラリ(Scikit-learnやTensorFlowなど)に渡すことができるため、Excelで前処理した後にまたPythonに読み込む、といった手間を省けます。

以上のように、Pandasを使うことで分析作業の効率化高度化が期待できます。実際、PythonとPandasの組み合わせは現在、金融、神経科学、経営、統計学、広告、Web解析など幅広い分野で活用されています (今さら聞けないPython - pandasを用いたデータ分析 #Databricks - Qiita)。Pandasは事実上、Pythonにおけるデータ分析の標準ツールであり、データサイエンスの現場でも欠かせない存在となっています。

これから章を追って、Pandasの基本から応用までを詳しく解説していきます。まずはPandasが提供するデータ構造であるSeriesとDataFrameについて、その基本的な使い方を見ていきましょう。


第2章 データ構造:SeriesとDataFrameの基本

Pandasの核となるデータ構造には**Series(シリーズ)DataFrame(データフレーム)**の2つがあります。これらはデータを格納し操作するためのオブジェクトであり、Pandasでのあらゆる処理はこの2つを中心に展開されます。まずはそれぞれの特徴と基本的な使い方を見てみましょう。

2.1 Series(シリーズ)とは

Seriesは、一列のデータを表す1次元のデータ構造です。イメージとしては「値の配列」に「インデックス(ラベル)」が付与されたものと言えます。各要素にはインデックスと呼ばれるラベルが割り当てられており、このラベルを使って要素にアクセスしたり操作を行ったりします。SeriesはPythonのリストやNumPyの配列に似ていますが、インデックスによるラベル付けができる点でより柔軟です。

例えば、曜日を表す文字列をSeriesにしたい場合を考えます。インデックスに曜日名、値にその曜日の略称を持つSeriesを作成してみましょう。

import pandas as pd

# 曜日名をインデックス、略称を値とするSeriesを作成
days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
abbr = ["Mon", "Tue", "Wed", "Thu", "Fri"]
ser = pd.Series(data=abbr, index=days)

print(ser)

上記のコードでは、`pd.Series()`関数に`data`として値のリスト、`index`としてインデックスのリストを渡しています。出力結果は以下のようになります。

Monday       Mon
Tuesday      Tue
Wednesday    Wed
Thursday     Thu
Friday       Fri
dtype: object

このように、各行にインデックス(左側)と値(右側)が表示され、Seriesが作成されました。最後の行の`dtype: object`は、このSeriesが文字列(Pythonのオブジェクト型)を要素として持つことを示しています。

Seriesでは、インデックスを指定して値を取り出すことができます。例えば、上記の`ser`から水曜日の略称を取得するには以下のようにします。

print(ser["Wednesday"])  # インデックス名でアクセス
Wed

文字列インデックスだけでなく、整数位置によるアクセスも可能です。先頭を0番目として、`ser[2]`と指定すれば3番目の要素(この例では`Wednesday: Wed`)を取得できます。ただし、整数でアクセスする場合とインデックスラベルでアクセスする場合で挙動が異なることがあるため注意が必要です(整数インデックスを持つSeriesの場合など)。この点については後述する`loc`と`iloc`で詳しく説明します。

Seriesは単なる配列以上の機能を持っています。例えば、ベクトル演算をサポートしており、数値のSeries同士であれば対応する要素ごとの演算が可能です。また、異なるインデックスを持つSeries同士の演算ではインデックスが自動的に揃えられます(アラインメント機能)。これはPandasのインテリジェントなデータアライメントと呼ばれる特性で、不揃いなデータ同士を演算する際にもインデックスを基準に自動的に結合してくれます (pandas - Wikipedia)。例えば次のような例を見てみます。

import numpy as np

# 数値のSeriesを2つ作成(インデックスを一部ずらしてみる)
s1 = pd.Series([1, 2, 3], index=["a", "b", "c"])
s2 = pd.Series([4, 5, 6], index=["b", "c", "d"])

print(s1 + s2)
a    NaN
b    6.0
c    8.0
d    NaN
dtype: float64

この例では、`s1`はインデックス`["a","b","c"]`、`s2`はインデックス`["b","c","d"]`を持っています。`s1 + s2`の結果では、インデックス`"a"`と`"d"`に対応する値が`NaN`(欠損値)となっています。これは、`s1`と`s2`のどちらか一方にしか存在しないインデックスについては対応する値が存在しないためです。`"b"`と`"c"`は両方のSeriesに存在するため、それぞれ`2+4=6`、`3+5=8`の計算結果が得られています。このように、Series同士の演算ではインデックスが自動的に揃えられ、不一致のところは欠損値(NaN)になるという挙動を示します。

まとめると、Seriesは**「インデックス付きの配列」**と言えるデータ構造で、インデックスによる要素アクセスや、同じインデックスを持つデータ同士の演算機能など、データ分析に便利な機能を備えています。

2.2 DataFrame(データフレーム)とは

DataFrameは、行と列の二次元構造を持つデータテーブルです。表計算ソフトのスプレッドシートや、データベースのテーブルに相当する構造で、Pandasの中心となるデータ構造です (pandas - Wikipedia)。DataFrameは複数のSeriesを束ねたものと捉えることもできます(各列がSeriesに対応します)。各列は異なるデータ型を持つことができ、例えば数値、文字列、日付などが混在した表形式データを格納できます。

DataFrameを作成する方法はいくつかありますが、典型的には辞書(dict)や二次元リスト、あるいは外部データソースから生成します。まずは簡単な例として、辞書からDataFrameを作成してみましょう。社員のデータ(氏名、年齢、部署)を持つDataFrameを作ります。

# 辞書からDataFrameを作成
data = {
    "Name": ["Alice", "Bob", "Charlie", "David"],
    "Age": [25, 30, 35, 40],
    "Department": ["Sales", "Engineering", "Engineering", "HR"]
}
df = pd.DataFrame(data)

print(df)
      Name  Age   Department
0    Alice   25        Sales
1      Bob   30  Engineering
2  Charlie   35  Engineering
3    David   40           HR

上記のように、`pd.DataFrame`に辞書を渡すとキーが列名、値のリストが各列のデータとなるDataFrameが作成されます。出力結果を見ると、4行×3列の表が表示されています。行にはデフォルトで0から始まる整数のインデックスが振られ(左端の0,1,2,3)、列見出しとして"Name", "Age", "Department"が表示されています。

DataFrameでは、各列がそれぞれSeriesになっています。例えば`df["Name"]`とすれば、"Name"列だけを抜き出したSeriesが取得できます。

names = df["Name"]
print(names)
0      Alice
1        Bob
2    Charlie
3      David
Name: Name, dtype: object

このように、列名をキーとして辞書感覚で指定することで列の抽出ができます。また複数の列を同時に選択したい場合は、列名のリストを渡します。

subset = df[["Name", "Department"]]
print(subset)
      Name   Department
0    Alice        Sales
1      Bob  Engineering
2  Charlie  Engineering
3    David           HR

この結果は元のDataFrameから"Name"と"Department"列だけを取り出した新たなDataFrameです。

行に関しても同様に操作できます。行をラベル(インデックス)で指定して取得するには`loc`を、行番号(位置)で指定するには`iloc`を使用します。例えば、インデックス`2`の行(Charlieのデータ)を取得するには次のようにします。

row = df.loc[2]
print(row)
Name          Charlie
Age                35
Department  Engineering
Name: 2, dtype: object

`df.loc[2]`の結果は、インデックスが`2`の行を表すSeriesが返ってきています(行方向のSeries)。これに対し、例えば`df.iloc[2]`でも同じ行が得られますが、`loc`はインデックスラベルで、`iloc`は整数位置で選択する点が異なります。上の例ではインデックスが0,1,2,3と整数なので`loc[2]`と`iloc[2]`は同じ結果ですが、もしインデックスが整数以外(例えば氏名がインデックスになっている等)であれば、`loc`と`iloc`の違いが重要になります。`loc`と`iloc`の詳しい使い分けについては次章で改めて説明します。

DataFrameにはデータの概観を確認するための便利なメソッドが用意されています。例えば、`df.head()`を使うと先頭数行を表示でき、`df.info()`を使うとデータ型や欠損値の有無などDataFrame全体の情報を得られます。`df.describe()`を使えば数値列の基本的な統計量(平均値、標準偏差、最小値、四分位数、最大値など)を一度に確認できます。これらのメソッドについても追って紹介しますが、まずは簡単に使ってみましょう。

print(df.head(2))    # 先頭2行を表示
print(df.describe()) # 基本統計量の表示
   Name  Age   Department
0  Alice   25        Sales
1    Bob   30  Engineering

             Age
count   4.000000
mean   32.500000
std     6.454972
min    25.000000
25%    28.750000
50%    32.500000
75%    36.250000
max    40.000000

上の`head(2)`の出力では、DataFrameの先頭2行のみが表示されています。`describe()`の出力では、"Age"列の要約統計量が表示されています("Age"列が数値データであるため)。このように、DataFrameは大きなデータでも一部を確認したり統計量を把握したりするのが簡単です。

まとめると、DataFrameは**「行と列からなるデータ表」**であり、Pandasにおける主要なデータ構造です。異なる型のデータを列として持てる柔軟性、高速なデータ操作、そして表形式データに対する豊富なメソッド群を備えています (pandas - Wikipedia)。次章では、このDataFrameを用いて実際にデータを読み込んだり加工・集計したりする基本操作を学びます。


第3章 データの読み込み、加工、クリーニング、集計

この章では、Pandasを使ったデータの取り扱いの基本を解説します。外部データの読み込み方法から始めて、データの加工(選択・フィルタリング・追加・削除)、クリーニング(欠損値や重複への対処)、そして集計(グループ化集計や基本統計量の算出)まで、一連の操作を順を追って見ていきましょう。

3.1 データの読み込み

Pandasは様々な形式のデータソースからデータを読み込むための関数を提供しています。その中でも代表的なものが**CSV(Comma-Separated Values)**ファイルの読み込みです。CSVはテキスト形式の表データで広く使われており、Pandasの`read_csv`関数で簡単に読み込むことができます。

# CSVファイルの読み込み例(ファイル名は仮定)
df_sales = pd.read_csv("sales_data.csv")

上記のように、`pd.read_csv("ファイルパス")`とするだけでCSVファイルをDataFrameとして読み込めます。実際にはオプション引数を指定して区切り文字を変えたりエンコーディングを指定したりできますが、基本的な使い方は非常にシンプルです。

CSV以外にも、Excelファイル(`.xlsx`)は`pd.read_excel`、JSONファイルは`pd.read_json`、SQLデータベースからの読み取りは`pd.read_sql`という関数が用意されています。これらを使うことで、様々なソースからデータを直接DataFrameとして取得可能です。例えばExcelファイルの読み込みは以下のようになります。

# Excelファイルの読み込み例(シート名を指定可能)
df_excel = pd.read_excel("data.xlsx", sheet_name="Sheet1")

同様に、SQLデータベースから直接データを読み込むこともできます。`pd.read_sql`関数を使う場合、データベース接続用のエンジンやコネクションオブジェクトと、SQLクエリまたはテーブル名を渡します。例えばSQLiteのデータベースからデータを読む場合:

import sqlite3
conn = sqlite3.connect("example.db")
df_from_db = pd.read_sql("SELECT * FROM customers", conn)

このコードでは、`example.db`というSQLiteデータベースファイルから`customers`テーブルの全行を取得し、DataFrameに格納しています。実際の業務ではPostgreSQLやMySQL等に接続することも多いですが、その場合も`pd.read_sql`にSQLAlchemyエンジンなどを渡すことで同様のことが可能です。

Pandasが対応している入力フォーマットは非常に多岐にわたります。CSV、Excel、SQLデータベースはもちろん、HTML、Stata、HDF5、Pickle、JSONなど多くの形式に対応しています (pandas - Wikipedia)。また、これらとは逆にDataFrameをファイルやデータベースに書き出す関数(`to_csv`, `to_excel`, `to_sql`など)も用意されています (今さら聞けないPython - pandasを用いたデータ分析 #Databricks - Qiita)。このような豊富な入出力機能により、異なるデータソース間でのデータのやり取りをPandasひとつで一貫して行うことができます。

実際のシナリオでは、まずPandasでデータを扱うためにCSVやExcelから読み込み、その後の分析結果を再びCSVやデータベースに書き出す、といった流れがよくあります。次節では、読み込んだデータに対してどのような基本操作ができるかを見ていきましょう。

3.2 データの確認と選択

データを読み込んだら、まず中身を確認することが大切です。前章でも触れましたが、主に使われるメソッドを改めて整理します。

  • `df.head(n)` : 先頭からn行を表示(nを指定しない場合はデフォルトで5行)。

  • `df.tail(n)` : 末尾のn行を表示。

  • `df.info()` : 行数・列数、各列のデータ型、非NULLの件数など全体の情報を表示。

  • `df.describe()` : 数値列の基本統計量を計算して表示。

  • `df.shape` : データフレームの形状をタプルで取得 (行数, 列数)。

  • `df.columns` : 列名の一覧を取得(Indexオブジェクトとして返されます)。

これらを活用して、読み込んだ直後のデータの概要を掴みます。例えば先ほど`pd.read_csv`で読み込んだ`df_sales`に対して:

print(df_sales.shape)
print(df_sales.columns)
print(df_sales.head(5))
print(df_sales.info())

といったコマンドを実行すれば、データ件数や列名、そして先頭部分の様子、各列の型と欠損の有無などがわかります。

次に、データから必要な部分を**選択(抽出)**する方法を見ていきます。基本的な選択方法として以下があります。

  • 列の選択: `df["ColumnName"]` または `df.ColumnName` で単一の列を取得。複数の列を取得する場合は `df[["ColA", "ColB"]]` のように列名のリストを渡します。

  • 行の選択: インデックスで選ぶ場合は `df.loc[index_label]`、位置(番号)で選ぶ場合は`df.iloc[row_index]`を使用します。スライスも可能で、例えば`df.loc[0:5]`はインデックス0から5までの行(0と5を含む)を取得します。

  • 条件によるフィルタリング: `df[ 条件式 ]`という形で、条件を満たす行だけを抽出できます。条件式には各列に対する比較を使い、例えば`df[df["Age"] > 30]`とすればAge列が30より大きい行のみからなるDataFrameが返されます。

これらを組み合わせることで、必要なデータを柔軟に取り出せます。順に例を示します。

列の選択例: 顧客データ`df_customers`から氏名とメールアドレスの列だけを取り出す。

selected = df_customers[["Name", "Email"]]

行の選択例: インデックス`100`の行、およびインデックス名が`"C003"`の行を取得する。

row_by_num = df_customers.iloc[100]   # 100番目(0始まり)の行
row_by_label = df_customers.loc["C003"]  # インデックスラベル"C003"の行

条件によるフィルタリング例: 年齢が20代(Ageが20以上30未満)の顧客だけを抽出する。

twenties = df_customers[(df_customers["Age"] >= 20) & (df_customers["Age"] < 30)]

ここで条件式に`&`(かつ)演算子を使っていますが、Pandasのシリーズ同士の論理積にはこの`&`を使い、論理和には`|`(パイプ)を使う点に注意してください(Pythonの論理演算子`and/or`はブーリアンの単一値に対して用いるものなので、Seriesには使えません)。

フィルタリング結果として得られる`twenties`は該当する行だけを含む新たなDataFrameです。元の`df_customers`とは独立したオブジェクトなので、`twenties`に対して行った変更が`df_customers`に影響を与えることはありません(ビューではなくコピーが返されます。ただしPandasの挙動として、条件抽出は基本コピーですが、SettingWithCopyWarningに注意すべきケースもあります)。

3.3 データの加工(追加・削除・並べ替え・リネーム)

データの一部を選択できるようになったら、次はデータの加工です。ここでは、列や行の追加・削除、値の置換、並べ替え(ソート)、列名の変更など、分析の下準備としてよく行われる操作について説明します。

3.3.1 列の追加と更新

新しい列を計算によって追加したい場合があります。例えば売上データにおいて、数量と単価から売上額を計算して列を追加する、といったケースです。Pandasでは、辞書に新しいキーと値を代入する感覚で列を追加できます。

# 単価(price)と数量(qty)から売上額(total)を計算して新しい列に追加
df_sales["total"] = df_sales["price"] * df_sales["qty"]

このように、存在しない列名`"total"`を指定して値を代入すると、新たな列がDataFrameに追加されます。右辺では既存の`price`列と`qty`列の要素同士を掛け合わせています。Pandasではこのような列同士の演算が要素ごとに自動的に適用されるため、ループを書かなくても全行の計算が一度に実行されます。

既存の列に対して値を更新(置換)する場合も同じ構文で代入すればOKです。例えば上記の計算で`total`列を追加した後、もし単価が税込価格で数量が個数だとして売上額も税込になっている場合、税抜売上額の列を追加したいとしましょう。消費税率を10%(0.1)として税込->税抜の変換を行います。

df_sales["total_without_tax"] = df_sales["total"] / 1.1  # 1.1で割って税抜金額算出

これで新たに`total_without_tax`列が追加されました。計算結果はfloat(浮動小数点)になりますが、必要に応じて丸めたり整数型に変換したりすることもできます。

3.3.2 行の追加と削除

行の追加は`loc`を使って新しいインデックスに値を代入する方法がありますが、通常は既存DataFrameと追加するデータ(もう一つのDataFrameやSeries)を結合する形で行います。行方向の結合には`pd.concat`関数を使うと便利です。これは第4章で結合について詳しく説明する際に改めて扱いますので、ここでは詳細は割愛します。

一方、行や列の削除には`drop`メソッドを使います。`df.drop()`は指定した軸(行か列)に沿って指定のラベルを削除した新しいDataFrameを返します(インplays in placeでなくコピーを返す点に注意。`inplace=True`オプションを指定すれば元のDataFrameを直接変更できますが、推奨はされていません)。

  • 列を削除: `df.drop("ColumnName", axis=1)` または `df.drop(columns="ColumnName")`

  • 行を削除: `df.drop("IndexName", axis=0)` または単に `df.drop("IndexName")`(デフォルトでaxis=0なので省略可)

複数の行や列を一度に削除する場合は、リストで指定します。例えば、`df.drop(["col1", "col2"], axis=1)`のようにします。

列削除の例: `df_sales`から中間計算列の`total_without_tax`を不要と判断して削除する。

df_sales = df_sales.drop("total_without_tax", axis=1)
# または df_sales.drop(columns="total_without_tax", inplace=True) としてもよい(非推奨のinplace使用)。

行削除の例: `df_customers`から特定の顧客ID(インデックス)を持つ行を削除する。

df_customers = df_customers.drop("C005")

上記ではインデックス `"C005"` の行が削除された新しいDataFrameが返され、それを`df_customers`に代入し直しています。

3.3.3 データの並べ替え(ソート)

データをある列の値に基づいてソート(並べ替え)したいこともよくあります。Pandasでは`sort_values`メソッドを使って、特定の列の値に従って並べ替えたDataFrameを取得できます。

# 年齢順(Age列)にソートしたデータを取得(昇順)
df_sorted = df_customers.sort_values("Age")

`sort_values("Age")`はAge列の値を基準に昇順(小さい順)に並べ替えます。降順にしたい場合は`ascending=False`を指定します。

# 年齢の降順にソート
df_sorted_desc = df_customers.sort_values("Age", ascending=False)

また、`sort_values`は`by=`引数にリストを渡すことで複数列でのソートも可能です。例えば「部署ごと、さらに年齢昇順」で並べたい場合は:

df_multi_sorted = df_customers.sort_values(by=["Department", "Age"])

この場合まずDepartment列で並べ、その中でAgeで並べるという多段のソートが行われます(デフォルトはいずれも昇順。個別に昇降順を指定したい場合は`ascending=[True, False]`のようにリストで指定)。

3.3.4 列名・行名の変更

列名(カラム名)を後から変更したい場合もあります。例えばデータソースによっては列名がわかりにくかったり、日本語名を英語名に直したいことがあるでしょう。Pandasでは`rename`メソッドを使って列名やインデックス名を変更できます。

# 列名の変更: "Name" 列を "FullName" に変更
df_customers = df_customers.rename(columns={"Name": "FullName"})

`rename(columns={旧名: 新名})`の形で辞書を渡せば複数同時に変更もできます。同様に行(インデックス)も`df.rename(index={"旧インデックス": "新インデックス"})`で変更可能です。

ただし、インデックス名の変更は頻繁には行いません。インデックスはデータのユニークなIDのような役割を持たせることもありますが、多くの場合はデフォルトの整数を使ったり、分析中はあまり意識せずに進めることも多いです。必要に応じて、例えば時系列データでは日時をインデックスに設定したり、ユニークキーをインデックスに設定したりといった使い方をしますが、それらは状況に応じて扱います。

ここまでで、PandasのDataFrameに対する基本的な操作(選択・追加・削除・並べ替え・リネーム)を紹介しました。これらはデータ分析における前処理の根幹となる部分です。続いて、データの質を担保するためのクリーニング作業について見ていきましょう。

3.4 データのクリーニング(欠損値・重複への対処)

生のデータにはしばしば「穴」や「汚れ」があります。つまり、欠損値(データが欠けている)や重複データ、異常値などです。ここでは主に欠損値と重複への対処方法について説明します(異常値の検出や処理はケースバイケースなので本書では割愛します)。

3.4.1 欠損値の確認と除去・補完

Pandasでは、欠損値(値が存在しないこと)を表すために特殊な値`NaN`(Not a Number)が使われます。`NaN`は数値データにおける欠損を示す浮動小数点値ですが、実際にはどんなデータ型でもPandas上では`NaN`(またはPythonの`None`)で欠損を表現します。データを読み込んだ際に、空白だった箇所などは自動的に`NaN`になります。

欠損の確認: 欠損値がどこに含まれているかを確認するには`isnull()`メソッドが便利です。`df.isnull()`は、DataFrameと同じ形状の真偽値(DataFrame)を返し、各要素が欠損値ならTrue、そうでなければFalseとなります。例えば、

df_sales.isnull().head(5)

のようにすれば、先頭5行について欠損箇所をTrue/Falseで表示できます。Trueがある場所が欠損です。また、`df.isnull().sum()`とすると、各列ごとにTrueの数、すなわち欠損値の数を合計してくれるので、どの列に欠損が何件あるか把握できます。`df.info()`にも各列の非Null件数が表示されるので、欠損の有無をざっと見るのに役立ちます。

欠損値の除去: 欠損が含まれる行または列をまるごと削除するには`dropna()`メソッドを使います。`df.dropna()`はデフォルトでは一つでもNaNが含まれる行を全て削除して新しいDataFrameを返します。大量の欠損があるような場合には思い切って行ごと削除することもあります。ただし、重要なデータが含まれる行まで消えてしまう可能性もあるため注意が必要です。引数に`axis=1`を指定すると列方向で欠損のある列を削除します。

例: 欠損値を含む行を削除

df_cleaned = df_sales.dropna()

例: 欠損値を含む列を削除

df_cleaned = df_sales.dropna(axis=1)

なお、`dropna`には`how`や`thresh`といった引数もあり、「全て欠損の行だけ削除」や「欠損がN個以上ある行を削除」のような細かい指定も可能です。

欠損値の補完(埋める): 欠損を削除するのではなく、何らかの値で置き換えることもよく行われます。これを補完や**埋める(fill)**と言います。典型的には平均値や中央値、0や文字列の場合は空文字など、データの内容に応じて埋める値を決めます。Pandasでは`fillna()`メソッドで欠損を一括置換できます。

例: 欠損値を0で埋める

df_filled = df_sales.fillna(0)

例: 列ごとに別の値で埋める(辞書を渡す)

df_filled = df_sales.fillna({"price": 0, "qty": 0, "customer": "不明"})

上記では、price列とqty列のNaNは0に、customer列(顧客名など文字列)のNaNは"不明"という文字列に置き換えています。このように`fillna`には列名と値のペアを辞書で渡すことで列ごとに異なる値で埋めることができます。

また、時系列データの場合は前後の値で埋める前方埋め(`method='ffill'`)や後方埋め(`method='bfill'`)もよく使われます。例えば株価データなどで営業日ベースのデータに休日を埋める際、直前の営業日の値で埋めるといったケースです。`df.fillna(method='ffill')`のように指定します。

3.4.2 重複データの検出と削除

同じデータが重複して含まれていることもあります。例えばデータ収集の過程で同じ顧客が二重に登録されてしまった等です。Pandasでは`duplicated()`メソッドで重複している行を検出できます。`df.duplicated()`は各行が以前に出現したのと同じ内容かどうかをTrue/Falseで返します(初回出現はFalse、2回目以降の重複に対してTrue)。`duplicated(keep='last')`とすると逆に後のを残して前のを重複とみなすこともできます。

重複行を削除するには`drop_duplicates()`メソッドを使います。これは`duplicated()`でTrueになった行を取り除いたDataFrameを返します。

例: 完全に重複する行を削除

df_unique = df_customers.drop_duplicates()

`drop_duplicates`はデフォルトで全列の値が全て同じ場合に重複と判断しますが、`subset`引数で特定の列だけ見て重複チェックをすることも可能です。例えば`subset=["Name", "Email"]`とすれば氏名とメールアドレスが両方同じなら重複とみなします。

重複の定義やどれを残すか(先に出たものを残すか後のを残すか)はシチュエーションによりますが、Pandasの上記機能でかなり柔軟に対応できます。

3.4.3 データ型の変換

データクリーニングの一環として、データ型の適切な変換も挙げられます。例えば数値だと思っていた列が実は文字列型になっていて計算できない、といったことが実務ではしばしばあります。また、桁数の大きな整数を節約のために32ビット整数型に落とす、カテゴリカルな文字列データを`category`型にするといった最適化目的の型変換もあります。

Pandasでデータ型を変換するには`astype`メソッドを使用します。例えば、文字列で入っている数字列を整数型に変換するには:

df["amount"] = df["amount"].astype(int)

とします。ただし、変換しようとする型に適さない値(例えば"N/A"のような文字列をintに変換など)があるとエラーになります。そういう場合は事前に欠損に置き換えるか、その値だけスキップするか検討する必要があります。`pd.to_numeric(df["col"], errors='coerce')`を使うと、変換不能なものをNaNにして数値型に変換することも可能です。

日付・時刻データについては`pd.to_datetime`関数でdatetime型(Timestamp型)に変換できます。一度datetime型にしておくと、後で述べる時系列操作が簡単に行えるようになるため、CSVなどから日時が文字列で読み込まれた場合は積極的に変換しておくとよいでしょう。

3.5 データの集計と要約

次に、データを集計・要約する操作を扱います。Pandasではグループ化統計量の計算が簡単に行えるよう最適化されています。主に使われるのは`groupby`による集計と、単純な列の統計量算出です。

3.5.1 基本的な統計量の算出

各列の平均や合計などを求める場合、Numpy同様のメソッドや関数が用意されています。たとえば、

  • `df["col"].mean()` : col列の平均値

  • `df["col"].sum()` : col列の合計値

  • `df["col"].min(), df["col"].max()` : 最小値・最大値

  • `df["col"].std(), df["col"].var()` : 標準偏差・分散

  • `df["col"].median()` : 中央値

  • `df["col"].value_counts()` : col列中の各値の出現回数

などがあり、Seriesに対してこれらを呼び出すと計算結果が得られます。DataFrameに対して呼び出した場合、通常は各列ごとに計算されます(例えば`df.mean()`は全数値列の平均をそれぞれ計算してSeriesで返します)。

既に紹介した`df.describe()`を使えば、主要な統計量を一度に確認できます。このメソッドは結果をDataFrameで返すので、プログラム内でその値を使いたい場合にも利用可能です(例えば`df.describe().loc["mean"]`で平均行のSeriesが取れる)。

3.5.2 グループ化と集計(groupbyによる集約)

データ分析では、カテゴリーごとの集計を行う場面がよくあります。例えば「支店ごとの売上合計」や「月ごとのアクティブユーザー数」などです。Pandasではこれを`groupby`機能で簡潔に実現できます。

`DataFrame.groupby(by)`メソッドでデータを指定したキーでグループ化し、その後に集計関数を適用します。典型的な使い方は以下のようなパターンです。

df.groupby("キーとする列名").集計関数()

集計関数とは上で挙げた`sum`や`mean`などです。また、一度に複数の集計を行いたい場合は`.agg()`メソッドを使って辞書で指定するか、`.groupby(...).mean()`のようにすれば数値列すべての平均を計算する、といったことも可能です。

具体例を示します。売上データ`df_sales`に、"store"列(店舗)と"amount"列(売上額)があるとしましょう。店舗ごとの売上合計を計算するには:

sales_by_store = df_sales.groupby("store")["amount"].sum()
print(sales_by_store)

これにより、store列でグループ分けを行い、各グループ内でamount列を合計した結果が得られます。`sales_by_store`には各店舗をインデックス、売上合計を値とするSeriesが返ってきます。例えば以下のような出力になるでしょう。

store
Shinjuku    1250000
Shibuya      830000
Akihabara    540000
Name: amount, dtype: int64

これをDataFrameとして得たい場合は、最初からDataFrame全体に`sum()`を適用するか、後で`reset_index()`を使ってインデックスを列に戻す方法があります。

sales_by_store_df = df_sales.groupby("store").sum().reset_index()

上記では数値列すべてについて店ごとの合計が計算され(例えば数量や件数など他の列も数値であれば合計される)、その結果をDataFrameとして、storeはインデックスではなく通常の列に戻しています。

複数キーでのグループ化も可能です。例えば支店と商品カテゴリの組み合わせごとの売上件数を知りたい場合:

counts = df_sales.groupby(["store", "category"])["order_id"].count()

のように、`by`にリストを渡せば多重にグループ化されます。結果はMultiIndex(階層的なインデックス)を持つSeriesになります。見た目は少し複雑ですが、「支店ごとにその中でカテゴリごと」といった粒度で集計できます。

さらに高度な集計として、一度に複数の集計指標を計算する方法があります。`agg`(aggregateの略)を使う方法です。例えば各支店ごとに売上額の合計と平均を同時に求めるには:

stats = df_sales.groupby("store")["amount"].agg(["sum", "mean"])
print(stats)

これにより、行がstore、列が"sum"と"mean"のDataFrameが得られます。自分で列名を変えたい場合は`agg({"sum": "sum", "avg": "mean"})`のように辞書で新しい名前をキーにして指定することもできます。

Pandasのgroupbyは「split-apply-combine」戦略と言われ、データをグループに**分割 (split)し、各グループに関数を適用 (apply)し、その結果を結合 (combine)**する処理を抽象化したものです (pandas - Wikipedia)。これにより、SQLのGROUP BYに相当する処理や、Excelのピボット集計に近いことが容易に実現できます。

以上で基本的なデータ集計操作も理解できました。ここまでの内容で、単一のデータセットを読み込んで前処理し、基本的な集計を行うところまで一通り可能になったはずです。次章では、さらに複数のデータを扱ったり、時系列データやピボットテーブルなど、より高度なデータ処理について解説します。


第4章 高度なデータ処理:結合、時系列処理、ピボットテーブルなど

この章では、Pandasを使ったより高度なデータ処理テクニックを紹介します。複数のデータセットを組み合わせる結合(ジョイン・マージ)、日付や時間を扱う時系列データの操作、そしてExcelのピボットテーブルのような集計を実現するピボットテーブルの作成について、それぞれ詳しく解説します。

4.1 複数のデータの結合(マージと連結)

現実のデータ分析では、単一のデータソースだけで完結することは稀で、しばしば複数のデータを組み合わせる必要が出てきます。例えば、「売上データ」と「顧客データ」を紐付けて分析したり、「前年データ」と「今年データ」を縦方向に結合して推移を比較したりといったケースです。Pandasにはこうした結合操作のために、**結合(join/merge)連結(concatenate)**の機能があります。

4.1.1 データのマージ(mergeによる結合)

マージ(merge)は、2つのデータフレームをキーとなる列(またはインデックス)に基づいて横方向に結合する操作です。関係データベースのJOINとほぼ同等のことができます。Pandasでは`pd.merge`関数、またはDataFrameの`merge`メソッドが利用できます。

基本的な構文は:

merged_df = pd.merge(df1, df2, how="種類", on="キー列名")
  • `df1`, `df2`は結合したいDataFrame

  • `how`は結合方法で、`"inner"`(内部結合:キーが一致するものだけ), `"left"`(左結合:左側df1のキーをすべて残す), `"right"`(右側df2のキーをすべて残す), `"outer"`(外部結合:両方のキーの和集合を残す)から選びます。デフォルトは`"inner"`。

  • `on`は結合に使うキー列の名前。df1とdf2で同名の列がキーの場合に指定します。異なる場合は`left_on`, `right_on`引数で別々に指定可能です。また、2つのDataFrameのインデックスをキーに使う`left_index=True`, `right_index=True`などの指定もできます。

例を考えましょう。`df_customers`(顧客マスターデータ、列: customer_id, name, etc)と`df_sales`(売上データ、列: order_id, customer_id, amount, etc)があり、customer_idで結合したい場合:

merged = pd.merge(df_sales, df_customers, how="left", on="customer_id")

こうすると、`df_sales`の各行に対応する顧客情報が結合されたDataFrame`merged`が得られます。`how="left"`なので、売上がある顧客については顧客情報が付与され、売上に現れない顧客(顧客マスターにいるが売上なし)は結果に含まれません(逆に売上にあるけど顧客マスターに存在しないIDがあれば、その行の顧客情報はNaNになります)。

結合後の列について、両方のDataFrameに同名の列が存在した場合、自動的に`_x`, `_y`が付いて区別されます(例えば両方に"address"列があれば、結果では"address_x", "address_y"のようになります)。`suffixes`引数でこの接尾辞は変更できます。

複数のキーで結合することもできます。その場合、`on=["key1", "key2"]`のようにリストで指定します。もちろん、キーが複数ある場合は両DataFrameにその組み合わせの列が存在する必要があります。

4.1.2 データの連結(concatによる結合)

連結(concatenate)は、データを上下もしくは左右に直結する操作です。Pandasでは主に`pd.concat`関数を使います。これは、インデックスや列を揃えるというよりは単純につなげるイメージです。上下方向の連結(行の追加)と、水平方向の連結(列の追加)両方に使われます。

基本構文:

combined = pd.concat([df1, df2, ...], axis=0)
  • `axis=0`は行方向の連結(縦につなげる)、`axis=1`にすると列方向の連結(横につなげる)となります。デフォルトは`axis=0`。

  • リスト中に複数のDataFrameを渡すことで、それらを順に連結します。

行方向の連結例: 1月のデータ`df_jan`と2月のデータ`df_feb`が同じ列構造であるとき、それらを縦につなげて1-2月まとめたデータにする。

df_1_2 = pd.concat([df_jan, df_feb], ignore_index=True)

ここで`ignore_index=True`を指定すると、新しい連結後のDataFrameでインデックスを再割り振ります(0,1,2,...と振りなおす)。指定しない場合、元のインデックスを引き継ぐため、インデックスが重複する可能性があります。同一の性質のデータを継ぎ足していく場合は`ignore_index=True`を指定することが多いです。

列方向の連結例: `df_left`と`df_right`がインデックスで揃ったデータで、それらを横に結合して一つのDataFrameにしたい場合。

df_wide = pd.concat([df_left, df_right], axis=1)

この場合はインデックスを基準に結合されます。もしインデックスが異なる場合、それらのインデックスの和集合が結果のインデックスとなり、どちらかに無い組み合わせはNaNで埋められます(`join='outer'`がデフォルト)。`join='inner'`とするとインデックスの共通部分だけが結果に残ります。

`concat`はシンプルですが、縦連結の場合は列名が一致していること、横連結の場合はインデックスが一致していることが望ましいです。そうでない場合、結果に思わぬNaNが入ったりするので注意してください。

4.1.3 結合時の注意点

  • 結合キーに重複がある場合、結果の行数が増えることがあります。例えば一対多の関係をmergeすると多側に合わせて複製されます(SQL JOINと同じです)。

  • `merge`も`concat`も、引数`validate`を指定して関係性のチェックができます(例: `validate="one_to_one"`なら1対1結合でないとエラーにする)。データの性質を把握している場合は活用すると良いでしょう。

  • `merge`でインデックスを使う場合、`left_index=True, right_index=True`と指定します。その際`on`は不要です。

  • 複数のDataFrameを順次`merge`する場合、一度に`pd.merge(df1, df2, ...)`と書けず、連鎖的に呼ぶ必要があります。例えば`df1.merge(df2, on="key").merge(df3, on="key")`のようにメソッドチェーンで繋げられます。

結合については以上です。Pandasの結合機能を使えば、ExcelでいうVLOOKUPやSQLのJOINに相当する処理を簡潔に記述できます (pandasのピボットテーブルでカテゴリ毎の統計量などを算出 | note.nkmk.me)(ExcelのVLOOKUPは片方向参照ですが、Pandasではより強力な結合が可能です)。次に、時系列データの扱い方を見てみましょう。

4.2 時系列データの操作

時系列データとは、日時に対応するデータ(例えば日毎の売上、分毎のセンサ計測値など)です。Pandasは時系列データを扱うための高度な機能を備えており、金融時系列の分析などから発展してきた経緯もあって非常に充実しています (今さら聞けないPython - pandasを用いたデータ分析 #Databricks - Qiita)。ここでは代表的な機能である日時型データの扱い、リサンプリング(頻度変換)、ローリング計算(移動窓処理)、シフトなどについて解説します。

4.2.1 日時型への変換とDatetimeIndex

Pandasで時系列データを扱う第一歩は、データの日時情報を**適切な型(datetime型)**にすることです。第3章でも触れましたが、`pd.to_datetime`関数を使うことで、文字列の日時をTimestamp型(Pandasのdatetime64[ns])に変換できます。例えば:

df["date"] = pd.to_datetime(df["date"])

とすると、"date"列がdatetime型になります。`infer_datetime_format=True`オプションをつけると自動でフォーマットを推測したり、`format="%Y-%m-%d"`のようにフォーマットを指定することも可能です。

DatetimeIndex: DataFrameのインデックスに日時を設定すると、時系列データとして特有の操作が可能になります (pandas.DataFrame, Seriesを時系列データとして処理 | note.nkmk.me)。インデックスを日時にするには、`df.set_index("date", inplace=True)`のように日時列をインデックスに設定します。また、日時をインデックスに指定して`pd.read_csv`等で読み込むことも可能です。

DatetimeIndexを持つDataFrame/Seriesでは、期間によるデータ抽出などが簡単になります。例えばインデックスが日時の場合:

  • `df["2020"]`とすると2020年のデータだけ抽出(年を指定)。

  • `df["2020-05"]`とすると2020年5月のデータだけ抽出(年月を指定)。

  • `df["2020-05-10":"2020-05-20"]`のように範囲指定も可能(この場合5月10日から20日まで)。

このように、文字列で年や年月を指定すると、その期間にフィルタされたDataFrame/Seriesを得られます。これはDatetimeIndexの強力な機能です。

4.2.2 リサンプリング(resample)と頻度変換

**リサンプリング(resampling)**とは、時系列データの頻度を変更する操作です (時系列データの処理と解析:PandasでのDateTime操作|Satoshi.A)。例えば日次データから月次データに集計し直す、または反対に1時間刻みのデータから1分刻みに補間する、といったことです。

Pandasでは`resample`メソッドを使います。`df.resample("ルール")`という形で頻度を指定し、その結果に対して集計や補間を行います。

  • ダウンサンプリング(頻度を粗くする: 例 日次->月次)では、`resample`に続けて`sum()`や`mean()`などで集計します。

  • アップサンプリング(頻度を細かく: 例 日次->時次)では、`resample`後に`ffill()`や`bfill()`などで欠損を埋めるか、`interpolate()`で補間します。

例: 日次データを月次に集計

# 日付をインデックスに持つSeries daily_sales(日次売上)があるとする
monthly_sales = daily_sales.resample("M").sum()

ここで`"M"`は月末(月単位)を意味する頻度指定です (pandasのピボットテーブルでカテゴリ毎の統計量などを算出 | note.nkmk.me)。`sum()`を適用することで、その月の合計が計算されます。結果の`monthly_sales`はインデックスが各月の月末日となるSeriesです。月初を使いたい場合は`"MS"`(Month Start)という頻度コードもあります。

他にも`"W"`(週)、`"D"`(日)、`"H"`(時間)、`"T"`または`"min"`(分)、`"S"`(秒)など様々な頻度コードがあります。また四半期や年なども指定可能です。詳細はPandasのドキュメントを参照してください。

例: 営業日データを月次平均に変換

monthly_avg = daily_sales.resample("M").mean()

単純に`mean()`にすればOKです。期間内にデータが存在しない場合は結果がNaNになります。

例: 日次データを営業日基準から毎日基準に変換(欠損を前日値で補完)

daily_filled = daily_sales.resample("D").ffill()

ここでは`"D"`(暦日)でリサンプリングし、`ffill()`(前方詰め)で間を埋めています。これにより、元データが営業日(平日)のみだった場合でも土日の分が直前の金曜の値で埋められ、毎日データに変換されます。

4.2.3 ローリング計算(移動平均など)

**ローリング(rolling)**とは、移動窓を設定して計算する手法です。例えば「過去7日間の移動平均」を各日に対して計算するといったことが代表例です。Pandasでは`df.rolling(window)`を使って実現します。

例: 7日間移動平均の計算

# daily_salesから7日間の移動平均Seriesを計算
moving_avg_7d = daily_sales.rolling(window=7).mean()

`window=7`は7データ分の窓(この場合7日)を取り、`mean()`でその平均を計算しています。結果は元と同じインデックスを持つSeriesですが、計算に十分なデータがない先頭部分(最初の6日分)はNaNになります。`min_periods`引数を指定すれば計算に必要な最小データ数を設定できます(例えば`min_periods=1`とすれば窓に1日でもあれば平均を出す)。

移動平均以外にも、`rolling`+`sum()`で移動和、`rolling`+`std()`で移動標準偏差など様々な計算が可能です。また`rolling`の中で`center=True`とすると中心化移動平均(現在の点を中心に前後に窓を取る)にもできます。

4.2.4 シフトとラギング

**シフト(shift)とは、データをある期間だけずらす操作です。時系列分析ではラグ(遅れ)**を取るとも言います。Pandasでは`df.shift(periods=n)`でインデックスをずらさずにデータだけをn要素分シフトできます。正のnで下方(未来方向)にシフト、負のnで上方(過去方向)にシフトです。

例: 前日比の計算

prev_day = daily_sales.shift(1)
daily_change = daily_sales - prev_day

`daily_sales.shift(1)`は売上Seriesを1日分下にシフトし、インデックスはそのままなので、結果は昨日の売上を今日の日付に対応付けたSeriesになります。これを引き算することで、本日の売上 - 昨日の売上を計算しています。この`daily_change`の先頭は昨日が存在しないためNaNになります。

時系列に特化した`shift`として、頻度単位でのシフトも可能です。`df.shift(freq="M")`のようにfreq引数を指定すると、インデックスそのものを月単位でシフトした新たなデータを生成します。例えば月次データSeriesに対して`shift(1, freq="M")`とするとインデックス(年月)が1ヶ月後にずれ、中身は同じというSeriesが得られます。これを使えば前年同月比の計算などが容易です。

4.2.5 時系列データの時間ゾーンやオフセット

高度な内容として、Pandasでは時間ゾーン(タイムゾーン)の取り扱いや、営業日カレンダーに沿ったオフセット(例えば営業日単位のシフトなど)もサポートされています。例えば、`tz_localize`や`tz_convert`メソッドでタイムゾーンのローカライズと変換ができます。また、`DateOffset`オブジェクトを使うと「月末営業日」などの特殊なシフトも可能です。

これらは必要に応じて学べば良いですが、Pandasが単純な日時計算以上の強力な機能を持っていることを押さえておきましょう (今さら聞けないPython - pandasを用いたデータ分析 #Databricks - Qiita)。

4.3 ピボットテーブルとクロス集計

Excelユーザーにはお馴染みのピボットテーブル機能も、Pandasで実現することができます (pandasのピボットテーブルでカテゴリ毎の統計量などを算出 | note.nkmk.me)。Pandasには`pivot_table`というメソッド/関数があり、これを使うと**指定したキーによる集計表(クロス集計表)**を簡単に作ることができます。`pivot_table`は内部的には`groupby`と`aggregate`を組み合わせた処理を行いますが、クロス集計の形(2次元の表形式)に結果を整形してくれる便利な関数です。

4.3.1 pivot_tableの基本

基本構文:

table = pd.pivot_table(data=df, values="集計したい値列", index="行方向のキー", columns="列方向のキー", aggfunc="集計関数")
  • `data`: 元のDataFrame。

  • `values`: 集計する値の列名。ここで指定した列の値を集計します(例えば売上額など)。

  • `index`: ピボットテーブルの行Indexとする列名(例えば顧客地域など)。

  • `columns`: ピボットテーブルの列Columnとする列名(例えば商品カテゴリなど)。

  • `aggfunc`: 集計関数。デフォルトは平均(np.mean)です。合計を取りたい場合は`aggfunc=np.sum`や`aggfunc="sum"`と指定します。

例: 商品カテゴリ×地域の売上合計ピボットテーブル

pivot = pd.pivot_table(data=df_sales, values="amount", index="product_category", columns="region", aggfunc="sum")

このコードは、`df_sales`データに対して商品カテゴリ毎(行)×地域毎(列)の売上額合計を集計した表を作成します。結果の`pivot`はDataFrameで、行ラベルが各商品カテゴリ、列ラベルが各地域、セルの値がその組み合わせの売上額合計になります。もし特定の組み合わせにデータがなければ、そのセルはNaNになります。

ピボットテーブルはExcelと同様に、集計軸を増やすことも可能です。`index`や`columns`にリストを渡せば複数軸でのピボットができます。また、`values`もリストで複数指定して、複数の値を集計することもできます(この場合、列はマルチインデックスになります)。

4.3.2 ピボットテーブルの追加オプション

`pivot_table`にはいくつか有用なオプションがあります。

  • `fill_value`: 結果の表でNaNを特定の値で埋めることができます。例えば0にしたい場合は`fill_value=0`とします。

  • `margins`: `True`にすると、行・列の小計/合計を計算して追加してくれます (pandasのピボットテーブルでカテゴリ毎の統計量などを算出 | note.nkmk.me)。行方向の合計は行名`All`、列方向も列名`All`で表示されます。

  • `dropna`: `True`にすると、全てNaNの列は結果から除外されます。カテゴリの組み合わせでデータが存在しない列などを落とすことができます。

例: 上記のピボットテーブルに小計をつけ、欠損セルは0で埋める

pivot = pd.pivot_table(df_sales, values="amount", index="product_category", columns="region", 
                       aggfunc="sum", fill_value=0, margins=True)

これで、`pivot`には行・列ともに`All`という集計行/列が追加され、欠損は0に置き換えられます。

4.3.3 クロス集計(crosstab)

ピボットテーブルと似た機能に、`pd.crosstab`関数があります。これは頻度集計(件数カウント)のクロス集計を行うのに便利です。基本的には`pivot_table`の特殊ケースで、`aggfunc='size'`(件数)を数えるものと考えられます。

構文:

ct = pd.crosstab(index=df["列名A"], columns=df["列名B"])

これだけで、列名A×列名Bの出現回数のクロス集計表が得られます。例えば`pd.crosstab(df["gender"], df["purchase"])`とすれば、性別×購入有無の表を作り、それぞれの組み合わせの頻度を表示します。

`crosstab`も`margins=True`で合計を付けたり、`values`と`aggfunc`で件数以外の集計も実はできますが、一般には`pivot_table`で十分です。

ピボットテーブル機能を用いると、Excelでやっていたような集計表をPython上で再現でき、さらに自動化や他の処理との組み合わせも容易になります (pandasのピボットテーブルでカテゴリ毎の統計量などを算出 | note.nkmk.me)。Excelの操作に慣れた人にとって、Pandasでのピボットは少しコードを書く手間がありますが、手順を自動化できる大きなメリットがあります。

以上、結合・時系列・ピボットといった高度なデータ処理テクニックを見てきました。次章では、Pandasを用いたデータの可視化について説明します。


第5章 データの可視化

データ分析において、**データの可視化(Visualization)**も重要なステップです。Pandas自体はグラフ描画の専門ライブラリではありませんが、matplotlibというPythonの標準的なプロットライブラリとの統合がなされており、Pandasのデータ構造から直接グラフを作成することができます。Pandasの`DataFrame.plot()`や`Series.plot()`メソッドを使うと、簡単に基本的なグラフを描画できます。ここではPandasを使った可視化の手法を紹介します(高度な可視化にはSeabornやPlotlyなどの専用ライブラリもありますが、本章ではPandas標準の機能に絞ります)。

5.1 Pandasによる基本プロット

PandasのDataFrameやSeriesには`plot`メソッドがあり、これを呼び出すとmatplotlibを利用してプロットを描画します。Jupyter Notebook環境などではインラインでグラフが表示され、スクリプトでは別途`matplotlib.pyplot.show()`を呼び出すことでウィンドウ表示されます。

折れ線グラフ(Line plot): SeriesまたはDataFrameのデフォルトは折れ線グラフです。時系列データや連続した数値の変化を見たいときに有用です。

import matplotlib.pyplot as plt

# 日次売上の折れ線グラフ
daily_sales.plot()
plt.title("Daily Sales")
plt.xlabel("Date")
plt.ylabel("Sales")
plt.show()

上記のように`Series.plot()`だけでもグラフが描けます。DataFrameの場合は各数値列が別の線として描画されます。例えば、複数店舗の売上推移を持つDataFrame(列が店舗、行が日付)なら、`df.plot()`で各店舗の線が一つのグラフに描かれます。

ヒストグラム: データの分布を見るヒストグラムも簡単に描けます。`df["col"].hist(bins=20)`のように`hist`メソッド(plotのkind指定でも可)を使います。

# 年齢分布のヒストグラム
df_customers["Age"].hist(bins=10)
plt.title("Age Distribution")
plt.xlabel("Age")
plt.ylabel("Frequency")
plt.show()

`bins`はビンの数(階級の数)です。戻り値はmatplotlibのAxisesなので、自由にカスタマイズもできますがここでは単純に使っています。

棒グラフ(Bar plot): カテゴリごとの値を比較するには棒グラフが適しています。Pandasでは`df.plot.bar()`または`df.plot(kind="bar")`で棒グラフになります。DataFrameの場合、x軸にインデックス、各列の値を棒の系列として描画します。

# 地域ごとの売上合計を棒グラフで表示
sales_by_region = df_sales.groupby("region")["amount"].sum()
sales_by_region.plot.bar()
plt.title("Sales by Region")
plt.xlabel("Region")
plt.ylabel("Total Sales")
plt.show()

Seriesで`plot.bar()`を呼ぶと、そのSeriesのインデックスをカテゴリ、値を高さとする棒グラフになります。上記例では`sales_by_region`が地域をインデックスとしたSeriesなので地域別売上棒グラフになります。

積み上げ棒グラフ: DataFrameの場合`df.plot.bar(stacked=True)`とすると列を積み上げた棒になります。カテゴリー内訳を表現する場合に便利です。

散布図(Scatter plot): 散布図は2つの変量の関係を表すのに使います。Pandasでは`DataFrame.plot.scatter(x="col1", y="col2")`で散布図が描けます。

# 広告費と売上の散布図
df.plot.scatter(x="Advertising", y="Sales")
plt.title("Advertising vs Sales")
plt.show()

引数`c`や`s`を指定すると点の色やサイズを変えることもできます(例えば第三の変数で色分けするなど)。

箱ひげ図(Box plot): 分布の要約(中央値や四分位範囲)を示す箱ひげ図も`df.boxplot()`で簡単に描けます。DataFrameの数値列について箱ひげ図が表示されます。

5.2 可視化のカスタマイズ

Pandas経由のプロットは、内部的にはmatplotlibのAxesオブジェクトを返します。そのため細かいカスタマイズ(タイトルの設定、軸ラベルの設定、凡例の位置調整など)はmatplotlibの関数を使って行います。前述の例でも`plt.title`, `plt.xlabel`などを使いました。`plot`メソッド自体も`title`, `xlabel`, `ylabel`, `legend`などのキーワード引数を受け取ることができますので、簡単な場合はそれらを指定することも可能です。

例:

sales_by_region.plot.bar(title="Sales by Region", color="skyblue", rot=0)
plt.ylabel("Total Sales")
plt.show()

ここではタイトルとバーの色、`rot=0`でx軸のラベル(地域名)の回転を0度(縦にならないように)にしています。

また、複数のプロットを一つの図に配置したい場合は、matplotlibの`plt.subplots`等を用いて軸を準備し、各軸に対して`df.plot(ax=ax_i, ...)`のように`ax`引数でどのAxesに描くかを指定します。

5.3 可視化のTips

  • 日本語フォント: グラフ中に日本語(例えばカラム名が日本語)を表示すると文字化けすることがあります。matplotlibでフォント設定を日本語対応フォントに切り替える必要があります。これは環境依存なので詳細は省きますが、例えばMatplotlibのrcParamsを設定するか、日本語対応フォント(IPAexゴシック等)をインストールして指定します。

  • 凡例とスタイル: `df.plot()`では自動的に凡例(各列名)が付きます。凡例は`plt.legend()`や`legend=True/False`引数で制御可能です。グラフのスタイル(線種、マーカーなど)は`style`引数や、matplotlibのデフォルトを変更することで調整できます。

  • Seabornとの連携: SeabornというライブラリはPandasのDataFrameを直接受け取って高度な可視化を行うことができます。例えば`sns.barplot(x="region", y="amount", data=df_sales)`のように書くと、Pandasデータフレームからすぐに棒グラフを作れます。Seabornは統計的なプロットに優れており、美しいデフォルトテーマを持つため、Pandasだけで物足りない場合はSeabornの利用も検討すると良いでしょう。

このように、Pandasは単体で高度なカスタムグラフを作ることは苦手ですが、matplotlibの力を借りて基本的な可視化を手早く行うことができます。解析中の簡易チェックや、レポート用の下書き的なプロットであればPandasの`plot`で十分です。最終的な仕上げにはmatplotlibやSeabornで調整すると良いでしょう。


第6章 業務効率化:Excelの代替としてのPandas活用

Pandasを学ぶ動機の一つに、「Excelで行っている業務を自動化・効率化したい」というものがあります。ここでは、Excelの代替としてPandasを活用する際に役立つポイントを解説します。Excelに慣れ親しんだユーザにとって、Pandasでどのように同等の操作ができるかを意識しながら説明します。

6.1 Excel的な操作とPandas

Excelでよく行われる操作と、そのPandasでの対応をいくつか挙げます。

  • フィルタ・ソート: Excelのオートフィルタ機能は、Pandasではブールインデックスによる抽出(第3章3.2節参照)で実現します。並べ替えは`sort_values`で行えます。これらをPythonスクリプトで記述しておけば、毎回手動でフィルタを設定し直す手間が省けます。

  • 数式による計算列: Excelでシートに数式を埋め込んで計算列を追加する代わりに、Pandasでは列演算や`assign`で計算列を作成します(第3章3.3.1節)。特に複雑な計算もPythonで書けるため、Excelの複雑なネストした数式より読みやすく保守しやすいコードにできます。

  • VLOOKUP/INDEX+MATCHによる参照: Pandasでは**merge(結合)**によって他のテーブルから情報を参照できます(第4章4.1節)。ExcelのVLOOKUPは左端列しか参照できませんが、Pandasのmergeは任意のキーで結合でき、複数キーや複数列の取り込みも自由自在です (pandasのピボットテーブルでカテゴリ毎の統計量などを算出 | note.nkmk.me)。これにより、マスターデータを紐付ける処理などを明確に書けます。

  • ピボットテーブル: Excelのピボットテーブル集計は、Pandasの`pivot_table`で対応します(第4章4.3節)。Excelではドラッグ&ドロップでできる操作ですが、Pandasではコードとして残るため、同じ集計を別のデータにもすぐ適用できますし、集計手順のドキュメントにもなります。

  • マクロ/VBA: Excelで繰り返し業務を効率化するにはマクロを組む方法がありますが、Python + Pandasは強力なスクリプト環境なので、Excelの操作を自動化するよりも、直接データ処理を自動化するアプローチにシフトできます。Pandasスクリプトを作成しておけば、それ自体が使い回せる「ツール」となります。

6.2 Excelファイルとの連携

業務上、Excelファイルそのものを扱う必要も多々あります。PandasはExcelファイルの読み書き機能を持っているため、Excelをデータソースやレポート出力として活用できます。

  • Excelからの読み込み: `pd.read_excel("file.xlsx", sheet_name="Sheet1")`でExcelファイルからDataFrameを読み込みます。複数シートを同時に読み込むこともでき、`sheet_name=None`とすると全シートを辞書で取得できます。

  • 既存Excelの加工: Pandasの`to_excel`は基本的に新規に書き出すことを想定しています。既存ファイルの特定セルに書き込んだり、書式設定まではできません(単純なデータ書き出しのみ)。高度なExcel操作が必要な場合は、OpenPyXLやXlsxWriterなどの専用ライブラリを併用することになります。例えば、グラフ付きテンプレートExcelにデータだけ差し替えたい場合、openpyxlで既存ブックを読み込んでPandasのデータをループで書き込む、といった方法を取ります。ただしそうした場合でも、データ処理自体はPandasで済ませ、結果を書くだけにするのがポイントです。

  • CSVとの使い分け: Excelは手軽ですが、Pandasで処理する中間データの受け渡しにはCSVやParquetなどの方が適しています。Excelは人が見る用途のフォーマットなので、データ連携では極力シンプルなテキストフォーマット(CSV)やバイナリ高速フォーマット(Parquet)を使い、最終出力や入力受付のときだけExcelにするという使い分けも考えられます。

6.3 自動化とスケジュール実行

Pandasを使った業務効率化の真価は、一度コードを書けば何度でも再利用できる点にあります。Excelで手作業していた作業をPythonスクリプトにすれば、日次・週次での自動実行も容易です。例えばWindows環境であればタスクスケジューラ、Linux環境ならcronなどを使ってスクリプトを定期実行できます。あるいはJupyter Notebook上で分析手順をPandasで記述し、そのNotebookを定期実行して結果を保存・メール送信するといった運用も可能です。

このように、Pandasは単に効率よくデータを扱えるだけでなく、プロセス全体の自動化を視野に入れることで、従来Excelで膨大な時間を要していた報告書作成作業などを劇的に効率化できます。

実例として、「毎朝メールで届くExcelデータを集計してレポートExcelを作成、関係者にメール送信する」という業務があるとします。これをPandasで自動化する流れは:

  1. Pythonスクリプトでメールを読み、添付Excelを`pd.read_excel`でDataFrameに読み込む。

  2. Pandasで前処理・集計(必要ならピボットやグラフ画像保存まで)を行う。

  3. 結果を`to_excel`で報告書Excelに書き出す(あるいは`to_csv`で簡易レポートにしてメール本文に載せることも)。

  4. Pythonからメール送信(smtplib等を使用)して結果を配信。

この一連をスケジュール実行すれば、人が関与するのはスクリプト保守とエラー対応くらいになり、日々の単調な作業から解放されます。

まとめると、PandasはExcelの代替として、

  • 再現性のある分析(同じ処理をブレなく繰り返せる)、

  • 大規模データ対応(Excelでは重い行数もプログラムなら処理可能)、

  • 自動化(スケジューリングや他システム連携によるノンタッチ運用)
    の観点で大いに役立ちます。初期投資としてコードを書く必要はありますが、一度作れば何度も使える蓄積となるため、中長期的に見れば業務効率は飛躍的に向上するでしょう (今さら聞けないPython - pandasを用いたデータ分析 #Databricks - Qiita)。


第7章 計算速度の最適化とパフォーマンス

Pandasは便利な高レベルAPIを提供していますが、場合によっては「処理が遅い」「重い」と感じることがあります。この章では、Pandasでの計算を高速化するテクニックや、逆に遅くなるパターンについて解説します。適切に使えば非常に高速なPandasですが、不適切な使い方をするとパフォーマンスが低下してしまうため、ポイントを押さえておきましょう。

7.1 遅くなる操作と高速化の基本原則

Pandasが遅くなる主な原因は、Pythonレベルでループを回してしまう処理です。Pandas/NumPyは内部をCで実装することで高速化しているため、できるだけベクトル演算(配列全体をまとめて処理)を使い、明示的なPythonループを避けることが重要です。

7.1.1 iterrowsやループ処理の禁止

特に有名なのが、DataFrameの各行を処理するために`iterrows()`や`itertuples()`を使ってforループを回すことです。これは非常に遅く、データ量が増えると実用的な速度ではなくなります。iterrowsは絶対に使ってはいけません! (pandasの高速化はiterrows解消が9割)。Pandasでは大抵の操作はループを使わず、ベクトル化されたメソッドやブールインデックス、`apply`/`agg`などで代替できます。どうしても複雑な処理で1行ずつ処理が必要に見えても、一旦考え方を転換して、「列全体に対する処理」に落とせないか検討してください。多くの場合、`np.where`や`map`、あるいは複数列をまとめて処理する`apply`(後述)で対応可能です。

例えば、「値がある条件を満たす場合に新しい列をTrue/Falseで作る」処理を考えます。悪い例:

# 悪い例: iterrowsで1行ずつ処理
df["flag"] = False
for i, row in df.iterrows():
    if row["col1"] > 0 and row["col2"] < 5:
        df.at[i, "flag"] = True

これは非常に遅いです。良い例:

# 良い例: ブール条件を直接適用
df["flag"] = (df["col1"] > 0) & (df["col2"] < 5)

これだけで全行に対して条件判定した結果のブールSeriesが得られ、それをそのまま新列にできます。ベクトル演算なので速く、コードも簡潔です。

(pandasの高速化はiterrows解消が9割)でも述べられている通り、iterrowsによるループを無くすだけで劇的に速度改善できるケースがほとんどです。

7.1.2 applyの使い所と注意

`DataFrame.apply`や`Series.apply`を使うと、行ごと・列ごとの任意の関数適用ができます。これも内部的にはループするので、Pythonで関数を実行する部分がボトルネックになる可能性があります。ただし、`iterrows`よりは多少マシで、一度にSeries単位で処理されるため、簡便さとのトレードオフで使われます。

`apply`を使う際のポイント:

  • できれば`Series.apply`ではなく、Series自体のベクトル演算やメソッドで済ませられないか考える。

  • 例えば文字列操作なら`Series.str`アクセサに用意されたメソッドを使う、日時変換なら`pd.to_datetime`を使う、カテゴリなら`map`や`replace`を使う、など。

  • それでも`apply`を書く場合、中の関数をできるだけPythonの組み込みやNumPy関数で構成し、高速にする。Pythonで重い処理をすると結局遅いです。

  • DataFrame.applyで`axis=1`(行方向)にすると各行をSeriesとして関数に渡すので遅めです(各行でdtypeが異なる可能性もあり、遅くなりやすい)。可能なら`axis=0`(列方向)にして処理するか、複数列を扱うなら`Series`を複数組み合わせるロジックを考える。

7.1.3 オブジェクトdtypeの扱い

Pandasの列の型が`object`(汎用オブジェクト型)になっていると、数値計算に比べて非常に遅くなります。典型例は文字列データや混合型データです。文字列操作はどうしても遅くなりがちですが、`Series.str`のメソッドは内部でC実装していたり多少工夫されています。とはいえ、大量のテキスト処理はPandasの得意領域ではないので、必要に応じてPythonの組み込み関数や別ライブラリ(例えば正規表現なら`re`モジュールを駆使する、テキスト処理専用ライブラリを使う)を検討しましょう。

また、`object`型の数値が入っている列(例えば数字だけどdtypeがobject)などは、前述のとおり`astype`で数値に変換するだけで随分速度が上がります。

7.1.4 グループ数が多いgroupbyは注意

`groupby`による集計は通常高速ですが、グループ数(ユニークキー数)が極端に多いとオーバーヘッドが増えます。例えば数百万行のデータをユニークキー数も数百万でgroupbyすると、ほぼ1行ずつ処理するのと同じになり非効率です。この場合、そもそもの問題設定を見直す(そんなgroupbyが必要か?)か、別のアルゴリズムを検討する余地があります。

7.2 メモリ使用とdtype最適化

速度と並んで問題になるのがメモリ使用量です。特に大きなデータを扱う際、メモリ不足で処理が遅くなったり最悪クラッシュしたりします。Pandasはデータをメモリ上に保持するので、効率的な型を使うことでメモリ節約と速度向上が図れます。

  • 不要な列は読み込まない: `read_csv`には`usecols`引数があり、必要な列だけ読み込むことができます。また`dtype`引数で列ごとの型を指定できます。最初から最適な型で読めば、読み込み時間も節約できます。

  • 整数型のダウンサイジング: デフォルトのintは64bit (np.int64)ですが、値が小さいなら32bitや16bitでも表現できます。例えば0〜100のデータなら`np.int8`(8ビット)でも十分です。手動で`df["col"] = df["col"].astype("int32")`などと変換することでメモリを半分にできます。ただしint型にNaNがあるとfloat型になってしまう問題があります。その場合`Int64`というPandasのnullableな整数型も使えます。

  • カテゴリカルデータ: 重複が多い文字列データは`category`型にすると効率的です。例えば性別やカテゴリ名など数種類しか値がない列をobject型文字列で持っていると、全要素が文字列として保持されますが、category型に変換すればユニークな値リスト+整数のコードで保持されます。メモリを大幅削減でき、比較やグループ化も速くなります。変換は簡単で、`df["col"] = df["col"].astype("category")`とするだけです。

7.3 処理のベクトル化とNumPy利用

前述のように、Pandasの高速処理にはベクトル化が鍵です。具体的なテクニックをいくつか紹介します。

  • NumPy関数の活用: PandasのSeriesやDataFrameは内部にNumPy配列を持っています。`df["col"].to_numpy()`で純粋なnumpy.ndarrayを取り出せます。NumPyのufunc(ユニバーサル関数、例えば`np.sqrt`や`np.sin`など)は配列全体に対してC実装のループを回すので高速です。PandasのSeriesにも基本的に同名のメソッドがありますが、NumPyの関数を直接使う方が速いこともあります。例えば`np.log(df["col"])`は`df["col"].map(np.log)`より速いでしょう。もっとも、最近のPandasはufuncの受け渡しも最適化されてきているので、大差ない場合も多いです。

  • np.whereで条件分岐: 複雑なif-elseを列に対して行うには、Pythonのループより`np.where`が有効です。例えば「col1 > 0ならA、そうでなければB」という新列を作るなら、`df["new"] = np.where(df["col1"] > 0, "A", "B")`とすれば高速に処理されます。これはブール配列を使ったベクトル化条件選択です。

  • マスク配列による代入: 条件に合致する要素だけ値を変えたい場合、`df.loc[条件, "col"] = 値`とすれば、これも内部的にはブール配列で一括代入します。ループで一つずつif判定して代入するのに比べ圧倒的に速いです。

  • 組み込みメソッド: Pandas/NumPyにはよく使われる処理を行う組み込みのメソッドが豊富にあります。それらは大抵C実装されているので高速です。例えば欠損を含む計算では`df.sum(skipna=True)`のようにskipnaオプションを使えばNaN無視でC実装の和を計算しますが、下手に自分でNaNを処理しながらループすると遅くなります。利用できるものは積極的に使いましょう。

7.4 並列処理・分散処理の活用

Pandas自体はシングルスレッドで動作します(一部、`read_csv`でマルチスレッド読み込みをすることがありますが、基本は単一CPUコアで処理)。大量データに対してマルチコアを活用したい場合は、自前でスレッドやマルチプロセスを使うか、別のライブラリを検討します。

  • swifter: Pandasのapply処理などを並列化してくれるライブラリとして`swifter`が知られています (pandasにそっと左手を添えるだけで処理速度が爆速に #Python - Qiita)。`df.apply(func)`を`df.swifter.apply(func)`に置き換えるだけで、データサイズに応じて並列実行や最適化をしてくれるというものです。状況によって効果は異なりますが、コード変更が少ない利点があります。

  • Pandarallel: 同様に`pandarallel`というライブラリもあり、`from pandarallel import pandarallel; pandarallel.initialize(); df.parallel_apply(func)`のように使います (2行追加するだけでPython Pandasを高速化するPandarallel)。

  • Dask: データが大きすぎてメモリに載らない場合や、並列分散処理したい場合は、Dask DataFrameを使う手もあります。DaskはPandasとほぼ同じAPIを持ちながら、データを分散させて並列処理できるフレームワークです。ただし全てのPandas機能が使えるわけではなく、多少制約があります。

  • Polars: 最近注目されているRust製のDataFrameライブラリPolarsも、Pythonから利用可能で、Pandasよりかなり高速との報告があります (pandasの高速化はiterrows解消が9割 - Zenn)。完全にPandasと互換ではありませんが、パフォーマンスが重要な場面では検討に値します。

こうした他ライブラリは、Pandasでどうしても速度が出ない場合の代替策となります。とはいえ、まずはPandasの使い方を見直してボトルネックを排除するのが先決です (pandasの高速化はiterrows解消が9割)。特にループをベクトル化するだけで「Pandasは遅い」という印象がガラッと変わるでしょう。

7.5 処理のプロファイリング

どこが遅いのか見当がつかない場合、Pythonのプロファイラ(例えば`cProfile`)や、Jupyter環境なら`%%timeit`マジックや`%%prun`マジックを使って特定部分の速度を計測することが有効です。時間のかかっている関数呼び出しが分かれば、上記のような改善策を適用できます。

また、メモリ消費については、Pandasでは`df.memory_usage(deep=True)`メソッドで各列のメモリ使用量を把握できます。どの列が巨大かが分かれば、dtypeの見直しや必要ない列の削除など対策が立てられます。

まとめ: Pandasの最適化においては、

  • 可能な限りベクトル化(ループしない)すること (pandasの高速化はiterrows解消が9割)、

  • データ型を適切に設定しメモリ効率を上げること、

  • 必要なら並列処理ライブラリを活用すること、
    がポイントです。これらを押さえれば、かなり大きなデータでもPandasで快適に扱えるようになるでしょう。


第8章 NumPyとの連携

Pandasはその基盤としてNumPyを使用しており、密接に連携しています。ここでは、PandasとNumPyの関係性や相互運用について解説します。

8.1 Pandasの内部はNumPy配列

PandasのSeriesやDataFrameの各列は、内部的にはNumPyのndarray(またはそれに準じたもの、例えばnullable型では若干異なりますが)を保持しています (pandas - Wikipedia)。つまり、PandasはNumPyのラッパーとも言える存在です。これは大きな利点で、NumPyの高速計算機能をPandasがそのまま享受していることを意味します。第7章で述べたように、Pandasが高速な理由はNumPyでベクトル演算をしているからです (今さら聞けないPython - pandasを用いたデータ分析 #Databricks - Qiita)。

具体的に確認してみましょう。

s = pd.Series([10, 20, 30, 40])
arr = s.to_numpy()
print(arr, type(arr))

このコードで、Seriesを`to_numpy()`でNumPy配列に変換しています。出力はおそらく `[10 20 30 40] <class 'numpy.ndarray'>` となり、Seriesが内部で持っているndarrayを取得できていることがわかります。`DataFrame.to_numpy()`とすると2次元のndarray(行列)になります。

8.2 NumPy関数をPandasオブジェクトに適用

NumPyの関数や演算はPandasのオブジェクトにも適用できます。多くのNumPy関数は、引数にPandasのSeriesやDataFrameを取ると、内部的にndarrayに変換して処理し、結果をPandasオブジェクトで返すようになっています。例えば:

import numpy as np
s = pd.Series([1.1, 2.5, 3.7, 4.6])
print(np.floor(s))

`np.floor`は各要素の小数点以下切り捨てを行うNumPy関数ですが、Series `s`を渡すことができます。結果は`0 1.0, 1 2.0, ...`というSeriesになります。つまり、NumPyのufuncはPandasのデータ構造でも動作する設計になっています。

これ以外にも、例えば`np.sin(df)`とすればDataFrameの全要素に対してサイン関数を適用し、同じ形のDataFrameを返します。`np.where`も先に述べたようにブールSeriesを処理できます。

Pandasの`index`や`columns`はNumPy配列ではなくPandas独自のIndexオブジェクトですが、`df.values`属性(推奨は`to_numpy()`)を使えば直接NumPy配列を取り出せます。古いコードでは`df.values`がよく使われましたが、`to_numpy()`の方が明示的で良いでしょう。

8.3 NumPyでできることはNumPyで

Pandasを使っていると、つい全てをPandasのメソッドでやろうとしがちですが、NumPyで容易にできる処理はNumPyを使う方がシンプルで高速な場合があります。例えば標準偏差の計算`df["col"].std()`はPandasでもできますが、デフォルトでは母集団の不偏標準偏差(N-1除数)になっていたり、厳密に同じ計算をしたいなら`np.std(df["col"].to_numpy(), ddof=0)`のようにNumPy側でやったほうが明確といったことがあります。

また、乱数生成線形代数演算などはNumPyの専売特許です。例えばランダムなデータでDataFrameを作りたいとき、まず`np.random.rand(100, 3)`で乱数の2D配列を作り、それをDataFrameにした方が効率的です。逆にPandasしか知らないと、`for`で乱数を100回発生させてSeriesにappendする…なんて無駄なことをしてしまうかもしれません。

NumPyとPandasはシームレスに行き来できるので、得意なほうを使う意識が大事です。特に大量の数値計算やシミュレーションはNumPyで行い、結果をPandasで整理すると良いでしょう。

8.4 ブロードキャストとアラインメント

NumPyにはブロードキャストといって、形状の異なる配列同士でも自動で拡張して演算する仕組みがあります。PandasのSeriesやDataFrameでも基本的にはこのブロードキャストが使えますが、一方で**インデックスによる自動揃え(アラインメント)**が入るため、NumPyの単純な位置対応とは異なる挙動になる場合があります (pandas - Wikipedia)。

例えばSeries同士の演算では、インデックスを基準に値を揃えて演算し、片方に無いインデックスは結果がNaNになることを第2章で説明しました(これはPandasのインテリジェントなデータアライメント機能です (pandas - Wikipedia))。NumPy配列同士なら単純に要素位置ごとに計算するので、Seriesを一度`to_numpy()`して計算すればインデックス無視の要素ごとの結果が得られます。こうした違いは知っておかないと戸惑うことがあります。つまり、

  • Pandasオブジェクト同士の演算: インデックス/列名で揃えて計算(align)。揃わないところはNaN。

  • NumPy配列同士の演算: 要素位置で計算。要素数が異なればブロードキャスト規則で拡張を試みる。

どちらが必要かで使い分けましょう。通常、データ分析ではインデックス揃えの挙動が便利なことが多いですが、高速に連続データを扱う場合はむしろ揃える処理が邪魔なこともあります。その場合はNumPy配列にしてから処理すると良いでしょう。

8.5 SciPyや他ライブラリとの連携

NumPyは様々なサイエンス系Pythonライブラリの土台となっています。Pandasのデータを他のライブラリに渡す際も、最終的にはNumPyの配列に変換して受け渡すことが多いです。例えば、

  • SciPy: 統計解析や科学計算の関数群。PandasのSeriesを受け取る関数もありますが、内部では`np.asarray`でndarrayにして使います。

  • Scikit-Learn: 機械学習ライブラリ。最近のバージョンではPandasのDataFrameをそのまま渡しても動くものもあります(列名を使って結果にDataFrameを返すなど)。しかし基本的には`fit`や`predict`メソッドにはNumPy配列を渡すことを想定しています。DataFrameのままでも動作しますが、メモリ効率や速度の観点で`df.values`にした方がよい場合もあります。

  • Matplotlib: 描画ライブラリ。こちらも最近は直接PandasのDataFrame/Seriesを受け取ってプロットしてくれます(Pandasが内部でMatplotlibに処理を委譲)。しかしMatplotlibの低レベルAPIを使う場合、自分でSeries.valuesを取ってプロットするケースもあります。

8.6 PandasとNumPyの住み分け

まとめとして、数値計算がメインであればNumPyラベル付きのデータ操作が必要ならPandasという住み分けがあります。Pandasを使うとどうしてもオーバーヘッド(インデックス管理など)があるので、単に大量の数値を高速に計算したいだけならNumPy/SciPyのほうが適しています。一方で、分析の途中で列名やインデックスで操作したり集計したりといった場面ではPandasのほうが圧倒的に便利です。適材適所で両者を組み合わせて使うことが、効率的なデータ分析につながります。


第9章 SQLや大規模データとの連携

Pandasは単体でデータを完結に扱うだけでなく、外部のデータベースやビッグデータ処理基盤とも連携して使われます。この章では、SQLデータベースとのやりとりや、大規模データを扱う際のアプローチについて解説します。

9.1 SQLデータベースとの連携

企業のデータは多くがRDBMS(リレーショナルデータベース)に格納されています。Pandasは、SQLとの接続インタフェースを持っており、直接データベースからクエリ結果を読み込んだり、DataFrameをテーブルとして書き出すことができます (pandas.read_sql — pandas 2.2.3 documentation)。

9.1.1 データベースから読み込む: read_sql

`pd.read_sql(query, con)`を使うと、SQLクエリまたはテーブル名で指定した内容をDataFrameとして読み込めます (pandas.read_sql — pandas 2.2.3 documentation)。ここで`con`はデータベースとのコネクション(またはSQLAlchemyのエンジン)です。SQLAlchemyを使えばPostgreSQLやMySQL、SQLiteなど様々なデータベースと接続できます。

例: PostgreSQLからデータを取得する場合

from sqlalchemy import create_engine
engine = create_engine("postgresql://user:password@hostname:5432/dbname")
df = pd.read_sql("SELECT * FROM sales WHERE date >= '2025-01-01'", engine)

このように、engine(接続情報)とSQL文を渡せばDataFrameに結果が入ります。`read_sql_table`や`read_sql_query`という関数もありますが、`read_sql`がそれらをラップしており、渡した文字列が"SELECT"文ならクエリと判断し、そうでなければテーブル名と判断して全件取得します (pandas.read_sql — pandas 2.2.3 documentation)。

取得したDataFrameは、さらにPandas内で加工・分析に利用できます。データベース上でできることはなるべくSQLで行い、結果だけPandasに持ってくると効率的です(データ量を減らせるため)。

9.1.2 データベースへ書き出す: to_sql

逆に、PandasのDataFrameをデータベースのテーブルに書き込むことも可能です。`df.to_sql(name, con, if_exists='replace')`のように使います (pandas.read_sql — pandas 2.2.3 documentation)。`name`はテーブル名、`con`は接続(engineなど)です。`if_exists`で既存テーブルがあればどうするか('replace':削除して新規作成, 'append':追加, 'fail':エラー)を指定できます。例えば分析結果を新しいテーブルにしてデータベースに保存し、別のアプリケーションで使ってもらう、ということもできます。

ただし、to_sqlで大量のデータを書き込むのは時間がかかります。数万件程度なら問題ないですが、数百万件を超えるような場合は、データベース側のバルクインサート機能を使った方が速いこともあります(to_sqlは内部で一行ずつINSERTを繰り返すかバッチインサートなので、データ量次第ではボトルネックになる)。

9.1.3 データベース連携の注意

  • データ型: データベースとPandasの型は完全には一致しません。特に日時型やカテゴリ型は注意です。read_sqlで取り込むと、データベース上のDATE/TIMESTAMPはPandasではdatetime64型になります。文字コードやエンコーディングにも注意(通常utf-8)。

  • メモリ: データベースから取得する行数が多すぎるとメモリに載りきらない可能性があります。その場合、クエリで期間を絞る、サンプリングする、あるいは次述のようにchunk単位で処理することを考えます。

  • セキュリティ: データベース接続情報(ユーザ名・パスワード等)は扱いに注意しましょう。ハードコーディングせず外部設定にするとか、読み込んだ後はengine.dispose()で接続を閉じるなど、基本的な配慮が必要です。

9.2 大規模データの扱い方

Pandasはメモリ上でデータを処理するため、扱えるデータサイズはマシンのメモリ容量に依存します。数十MB〜数百MBのデータなら問題なく扱えますが、何GBものデータになると厳しくなってきます。しかし、それでもPandasを使って大規模データの分析を行いたい場合、いくつかアプローチがあります。

9.2.1 分割して処理する(Chunking)

Pandasの`read_csv`や`read_sql`には`chunksize`という引数があり、これを指定するとデータを部分ごと(チャンク)に分けて読み込むことができます。イテレータを返すので、それをループして各部分を順次処理する形になります。

例: 大きなCSVをチャンク読み込みして集計だけ行う

iter_csv = pd.read_csv("large_data.csv", chunksize=100000)
total_sum = 0
for chunk in iter_csv:
    total_sum += chunk["amount"].sum()
print("Amount total:", total_sum)

このようにすれば、一度に100000行ずつ読み込んでその範囲の集計をし、結果を足し合わせることができます。ピークメモリ使用量はチャンクサイズ次第なので、大きすぎない範囲にすればメモリ不足を防げます。

注意点として、チャンク処理では状態を自分で保持する必要があります。上記の例では単純に合計なので足すだけですが、もっと複雑な処理ではループの外でデータを蓄積したりする必要があります。例えば平均と標準偏差を求めるには、全データの個数・合計・2乗和をチャンクごとに足して最後に計算、など工夫が要ります。

9.2.2 サンプリングや集計でデータ量を減らす

すべてのデータを細部まで分析する必要がない場合、サンプリング(一部抽出)や事前集計でデータ量を減らすことも検討しましょう。例えば10億行のログデータから傾向を見るのに、ランダムに100万行抽出してPandasで分析しても有益なことがあります。あるいは、月別集計値にしてしまえば行数が激減します。

データベースを使えるならSQLでサンプリングクエリや集計クエリを実行して、結果だけPandasに渡すのが効率的です。Hadoopなどのビッグデータ基盤があるなら、MapReduceやSparkで前処理してからPandasに渡す方法もあります。

9.2.3 DaskやSparkとの連携

前述したDask DataFrameは、大量のデータを複数のPandas DataFrame(パーティション)に分けて並行処理できるライブラリです。Pandasとほぼ同じ文法で書けるため、メモリに載らない場合の代替として有用です。Dask DataFrameから小さなsubsetを抽出してPandasに渡すという使い方もできます。

PySpark(SparkのPython API)もDataFrameの操作が可能で、Pandasに似た文法(Spark SQLの文法ですが)を提供します。Sparkは分散処理で大規模データを扱えるので、クラスタ環境があるならSparkで大まかな処理をして結果をPandasに取り込む方法も取れます。

Pandas自体にも、Sparkや大規模データ処理との橋渡し機能が少しずつ追加されています。例えば`pandas.read_parquet`でApache Parquetフォーマット(列指向で圧縮された大規模データ向けフォーマット)を直接読み込めますし、SparkからPandasを使うAPI(pandas_udfなど)も提供されています。

9.2.4 メモリ増設 or クラウド利用

これは手っ取り早い方法ですが、扱うデータに対してマシンのメモリが少なすぎるなら、ハードウェアリソースを増やすのも現実解です。例えば手元PCで苦労するより、クラウド上の大メモリマシン(RAM数百GB級)を一時的に使ってPandas処理することもできます。Pandas自体はメモリさえあれば数千万行のデータも操作可能です。ただしメモリ増設にも限界はありますし、あまり巨大データをPandas単体で処理するのは得策ではないケースも多いです。

まとめ: Pandasはミリオン(百万)行規模までは比較的楽に扱えますが、ビリオン(十億)行となると工夫が必要です。データの分割・抽出・他ツールとの協調を駆使して、大規模データの中からPandasが扱えるサイズに落とし込むスキルも、実務では重要になります (今さら聞けないPython - pandasを用いたデータ分析 #Databricks - Qiita)。


第10章 機械学習での利用

機械学習のプロジェクトにおいて、Pandasは**前処理(データ準備)**のフェーズで大活躍します (機械学習チュートリアル① - はじめに 〜 pandas入門) (機械学習チュートリアル① - はじめに 〜 pandas入門)。この章では、機械学習でPandasがどのように使われるか、その典型的なパターンを紹介します。

10.1 特徴量エンジニアリングとPandas

機械学習モデルに投入するデータ(特徴量)を作成・加工する作業を特徴量エンジニアリングと言います。Pandasは、元の生データから特徴量を作り出す過程でほぼ標準的に使われます。

例えば、以下のようなタスクがあります。

  • 欠損値の処理: 機械学習モデルは欠損を扱えない場合が多いので、Pandasで`fillna`等を使って補完します。

  • カテゴリ変数のエンコーディング: 文字列で表されるカテゴリデータ(例: "Male"/"Female", "Urban"/"Rural")を数値に変換します。Pandasでは`pd.get_dummies`を使ってワンホットエンコーディング(ダミー変数化)することができます。また、前述のcategory型を使ってラベルエンコーディング(カテゴリに番号を振る)も可能です。Scikit-learnにもエンコーダがありますが、Pandasで済ませることもあります。

  • 新しい特徴量の作成: 複数の列から計算で新しい列を作る(第3章3.3.1節のような列演算)。例えば金額×数量から総額、2つの日付から日数差を計算、文章から文字数を計算など、Pandasで簡単に行えます。

  • 集計による特徴量: データがリレーショナルになっている場合、groupbyして集計した結果を特徴量として付与することがあります。例えばユーザごとの過去購入回数を特徴量にするなら、`df.groupby("user_id")["purchase_id"].count()`を計算し、それをユーザIDで元データにマップ(merge)すれば「そのユーザの購入回数」という列が得られます。Pandasの集計と結合の力で簡潔に実装できます。

  • 時系列特徴量: 時系列データでは、ラグ特徴量(過去n時点の値)、移動平均、頻度などを特徴量に加えることがあります。Pandasの`shift`や`rolling`, `resample`を使って算出可能です。

このように、Pandas = 特徴量エンジニアリングツールと言っても過言ではありません。実際、多くのKaggleの解答や企業のMLパイプラインで、特徴量作成部分はPandasコードが大量に出てきます。

10.2 特徴量とターゲットの準備

機械学習ライブラリ(Scikit-learnやTensorFlow/Keras等)にデータを渡すときは、通常特徴量行列X(2次元、サイズ: サンプル数 x 特徴量数)とターゲットベクトルy(1次元、サイズ: サンプル数)に分けます。Pandasでは、DataFrameをXに、Seriesをyに対応させる形で準備します。

# df 全体から、目的変数 'Outcome' 列をyに、それ以外をXに分ける
X = df.drop("Outcome", axis=1)
y = df["Outcome"]

ここでXはDataFrame、yはSeriesです。Scikit-learnの`train_test_split`関数はPandasオブジェクトを直接受け取っても動作し、返り値もPandasオブジェクトになります。

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)

train_test_splitはインデックスも一緒に分割してくれますが、インデックスは特に意味がないなら捨てても構いません。もし後でインデックスで結果を紐付けたい場合は残しておきます。

10.3 モデルへの受け渡し

最近のScikit-learnはPandasのDataFrameを受け取ると内部でNumPy配列に変換しつつも列名情報を保持して、例えば`RandomForest.feature_importances_`を出力する際にその列名を付けてSeriesを返すなどの工夫がなされています(ただし全てのモデルでそうではありません。一部機能です)。

一般的には、`model.fit(X_train, y_train)`とすればX_train(DataFrame)・y_train(Series or array)からモデル学習します。内部で`np.asarray`されるため、DataFrameであっても列名はモデル内部では使われません。

モデル予測後に結果をPandasに戻す例:

y_pred = model.predict(X_test)
result_df = X_test.copy()
result_df["Actual"] = y_test
result_df["Predicted"] = y_pred

これで、各サンプルについて実際の値と予測値を並べたDataFrameが得られます。インデックスが元のままなので、どのデータかわかりやすいです。

10.4 Pipelineとの連携

Scikit-learnのPipelineを使って前処理からモデルまで組み立てる場合でも、Pandasは基本データ供給源となります。例えば、Pipeline内で自作の変換クラスにPandasの操作を書くことも可能です。ただしPipeline内ではNumPy arrayが渡される前提になっているため、カスタムTransformerでPandasを使いたい場合は工夫が必要です(ColumnTransformerで特定列を選択して処理するなど)。

最近は、Scikit-learnのColumnTransformerで`"passthrough"`を指定してDataFrameの特定列をそのまま渡すと、後段でその列名を使った処理ができるといった、Pandasを前提としたPipelineの組み方も提案されています。しかし、そうした部分はまだ発展途上であり、Pandasのデータを一度NumPyにしてから再度Pandasに戻す手間を感じることも多いです。

10.5 pandas_profilingなど

Pandas周辺には、機械学習の前にデータを自動探索してくれるライブラリもあります。その代表がpandas_profiling(現`ydata-profiling`)です。`ProfileReport(df)`を実行すると、dfの各列の分布や相関、欠損状況などをまとめたレポートを自動生成してくれます。こうしたツールを使うと、どの特徴量が有用そうかを事前に絞り込む助けになります。

10.6 実運用での注意

Pandasで前処理をしてからモデルに渡すフローは、モデル開発時には便利ですが、実運用で膨大なリアルタイムデータに適用する際は、スケーラビリティの観点で課題もあります。例えば大量のデータを逐次予測する場合、Pandasの処理がボトルネックになりうるため、可能なら前処理部分をNumPyベースやSparkに移行したり、モデルサービングでは特徴量計算を別システムでやってからモデルだけ動かすなどの工夫がなされます。

しかし、開発段階ではPandasで素早くデータを触り、仮説を検証することが大事です (機械学習チュートリアル① - はじめに 〜 pandas入門)。Pandasを駆使して得られた洞察や特徴量は、のちに生産環境に移植する際の土台となります(場合によっては、そのままPandasスクリプトをバッチで回すこともありますが)。機械学習エンジニアにとって、Pandasはデータ理解と前処理の相棒と言えるでしょう。


第11章 Pandasでできること一覧

最後に、本書で紹介したPandasの機能やできることを一覧形式でまとめます。Pandasの持つ多彩な機能を再確認し、必要なときに思い出せるようにしておきましょう。

  • データ構造提供: ラベル付き1次元配列のSeries、ラベル付き2次元表のDataFrameを提供 (pandas - Wikipedia)。高速で効率的なデータ操作を可能にするDataFrameオブジェクト (pandas - Wikipedia)。

  • 柔軟な入出力: CSV、TSV、Excel、JSON、SQL、HTML、Stata、HDF5など多様なフォーマットからのデータ読み書きに対応 (pandas - Wikipedia) (今さら聞けないPython - pandasを用いたデータ分析 #Databricks - Qiita)。

  • データ検査: `head`, `tail`, `info`, `describe`, `shape`, `dtypes`などによるデータの概観把握。

  • 選択・フィルタリング: 列名や行インデックスによる抽出(`[]`, `.loc`, `.iloc`)、ブーリアンインデックスによる条件抽出。

  • 欠損値処理: 欠損の検出(`isnull`), 除去(`dropna`), 補完(`fillna`) (pandas - Wikipedia)。

  • 重複処理: 重複の検出(`duplicated`), 削除(`drop_duplicates`)。

  • 並べ替え: 値によるソート(`sort_values`), インデックスによるソート(`sort_index`)。

  • 列・行の操作: 列の追加(直接代入, `assign`), 削除(`drop`), 列名変更(`rename`), インデックス設定・リセット(`set_index`, `reset_index`)。

  • 統計量計算: 合計・平均・中央値・分散・標準偏差・最小/最大・四分位数など各種集計(`sum, mean, median, var, std, min, max, quantile`)。値カウント(`value_counts`)。記述統計(`describe`)。

  • グループ化集計: groupbyによるsplit-apply-combine操作 (pandas - Wikipedia)。`agg`による複数集計。ピボットテーブルによるクロス集計 (pandasのピボットテーブルでカテゴリ毎の統計量などを算出 | note.nkmk.me)。

  • 結合: 関係データのマージ(`merge`, `join`)による結合 (pandasのピボットテーブルでカテゴリ毎の統計量などを算出 | note.nkmk.me)。インデックス、キーによる多様な結合(内部・外部・左・右結合)。

  • 連結: 同種データの縦方向・横方向連結(`pd.concat`)によるDataFrameの結合。

  • 時系列処理: DatetimeIndexによるスライス(時間による部分選択) (pandas.DataFrame, Seriesを時系列データとして処理 | note.nkmk.me)。頻度変換とリサンプリング(`resample`) (時系列データの処理と解析:PandasでのDateTime操作|Satoshi.A)。シフト(`shift`)によるラグ作成。移動集計(`rolling`)による移動平均・移動和等 (今さら聞けないPython - pandasを用いたデータ分析 #Databricks - Qiita)。時間ゾーン処理(`tz_localize`, `tz_convert`)。期間/季節性の扱い(resample, offset)。

  • 描画: 簡易プロット機能(`plot`メソッドによる折れ線グラフ、棒グラフ、ヒストグラム、箱ひげ図、散布図など)。Matplotlibとの連携によるカスタマイズ。

  • データ整形: 転置(`T`)、スタック・アンスタック(`stack`, `unstack`)によるMultiIndexとの変換。meltやwide_to_longによる縦持ち・横持ち変換。

  • カテゴリデータ: カテゴリ型(`astype('category')`)によるメモリ節約と効率化。ダミー変数化(`pd.get_dummies`)。

  • 文字列操作: `str`アクセサによるベクトル化文字列操作(大文字小文字変換、部分一致、正規表現抽出、置換など)。

  • 適用処理: `apply`, `applymap`, `map`による任意関数の適用。ただし、ベクトル化や組み込みを使う方が高速な場合が多い。

  • 並列/高速化: 内部的にCython/Cで最適化された演算 (今さら聞けないPython - pandasを用いたデータ分析 #Databricks - Qiita)。大規模データに対するラベルベースのスライス・ファンシーインデックス (今さら聞けないPython - pandasを用いたデータ分析 #Databricks - Qiita)。ユーザー側ではchunk読み込み、dtype適切化、必要ならDaskなどの活用。

  • NumPy連携: NumPy配列への変換(`to_numpy`)、ufuncの適用(np関数を直接利用)、ブロードキャスト演算。互換性の高い操作性。

  • SQL連携: データベースからの読み書き(`read_sql`, `to_sql`) (pandas.read_sql — pandas 2.2.3 documentation)。

  • エコシステム: 他ライブラリ(SciPy, Scikit-learn, Matplotlib, Seaborn, StatsModelsなど)との組み合わせ。データを供給し、結果を受け取るハブ的役割。

  • その他: ピボットテーブルの小計(`margins=True`)、マルチインデックス操作、条件に基づく更新(`where`, `mask`)、辞書やSeriesによる値置換(`replace`)、ランキング(`rank`)、一意な値(`unique`, `nunique`)など、多数の便利関数。

Pandasは非常に多機能であり、ここに挙げたもの以外にも細かな機能が豊富に存在します。公式ドキュメントやリファレンスを参照すると、各関数の詳細やより高度な使い方が載っています。まずは本書で紹介した主要な機能を押さえ、実際の分析で活用してみてください。 その過程で、「もっと○○がしたい」というニーズが出てきたら、その都度Pandasで可能か調べてみましょう。きっとPandasは皆さんのデータ分析における強力な助っ人となってくれるはずです。

いいなと思ったら応援しよう!