Pandas 空のDataFrameを徹底解説:作成から判定、実例まで
データ分析や処理において、PythonのPandasライブラリは非常に強力なツールです。DataFrameはPandasの中核をなすデータ構造であり、表形式データを扱う際に欠かせません。通常、DataFrameはファイルからデータを読み込んだり、既存のデータ構造から生成したりして作成しますが、時には「空のDataFrame」が必要になることがあります。
空のDataFrameは、一見すると何の役にも立たないように思えるかもしれません。しかし、特定のプログラミングパターンや処理フローにおいては、非常に有効な、あるいは不可欠な要素となります。例えば、処理結果を段階的に蓄積する場合や、関数の戻り値としてデータが存在しなかったことを示す場合などです。
この記事では、Pandasにおける空のDataFrameについて、その「作成方法」「判定方法」「なぜ必要なのか(ユースケース)」「具体的な取り扱い方法」「実例」に至るまで、約5000語のボリュームで徹底的に解説します。この記事を読むことで、空のDataFrameを自在に操り、より堅牢で柔軟なデータ処理コードを書けるようになるでしょう。
はじめに:Pandas DataFrameと空のDataFrameとは?
Pandas DataFrameは、行と列を持つ2次元のラベル付きデータ構造です。スプレッドシートやデータベースのテーブルのようなものと考えられます。各列は異なるデータ型(数値、文字列、ブール値など)を持つことができ、柔軟なデータ操作機能(フィルタリング、集計、結合など)を提供します。
空のDataFrameとは、行データが一切含まれていないDataFrameのことです。列だけが存在する場合もあれば、列すら存在しない場合もあります。しかし、重要なのは「行の数(データ件数)がゼロである」という点です。
なぜ空のDataFrameが必要なのでしょうか?いくつかのシナリオが考えられます。
- 結果の初期化: 複雑な処理やループ処理の結果を格納するための「空っぽの箱」として初期化し、後からデータを追加していく場合。
- 条件に合致するデータがない場合: フィルタリングなどの処理を行った結果、条件に合致するデータが一つもなかった場合に、その結果を空のDataFrameとして表現したい場合。
- 関数の戻り値: データを検索したり処理したりする関数が、対象データを見つけられなかった場合に、エラーを発生させる代わりに空のDataFrameを返すことで、呼び出し元で結果の有無をシンプルに判定できるようにする場合。
- 構造のテンプレート: 特定の列名とデータ型を持つDataFrameが必要だが、現時点ではデータがない場合に、構造だけを定義しておく場合。
これらのシナリオに対応するため、空のDataFrameの作成方法と、それが本当に空であるかを正確に判定する方法を知っておくことは非常に重要です。
1. 空のDataFrameの作成
Pandasで空のDataFrameを作成する方法はいくつかあります。目的や、あらかじめ分かっている情報(列名、データ型など)に応じて使い分けることができます。
1.1. 最も基本的な方法:pd.DataFrame()
引数を何も指定せずにpd.DataFrame()
を呼び出すと、行も列も全く含まない、完全に空のDataFrameが作成されます。
“`python
import pandas as pd
行も列も含まない、完全に空のDataFrameを作成
df_empty_basic = pd.DataFrame()
print(df_empty_basic)
print(df_empty_basic.shape) # (0, 0)
“`
出力は以下のようになります。
Empty DataFrame
Columns: []
Index: []
(0, 0)
これは最もシンプルですが、通常は列名やデータ型をあらかじめ定義しておきたいケースが多いでしょう。
1.2. 列名を指定して空のDataFrameを作成
空のDataFrameを作成する際によく行われるのが、使用する予定の列名をあらかじめ指定しておくことです。これは、後からデータを追加する際に、列の構造が保証されるため便利です。columns
引数に列名のリストを指定します。
“`python
列名を指定して空のDataFrameを作成
df_empty_with_cols = pd.DataFrame(columns=[‘ID’, ‘Name’, ‘Value’])
print(df_empty_with_cols)
print(df_empty_with_cols.shape) # (0, 3)
print(df_empty_with_cols.dtypes)
“`
出力例:
Empty DataFrame
Columns: [ID, Name, Value]
Index: []
(0, 3)
ID object
Name object
Value object
dtype: object
この例では、行数は0ですが、列数は3であり、指定した列名が設定されています。ただし、この方法で作成した場合、デフォルトでは各列のデータ型はobject
になります。後から追加されるデータの型によって自動的に推論されるか、または明示的に型変換する必要があります。
1.3. 列名とデータ型を指定して空のDataFrameを作成
空のDataFrameに後からデータを追加していく場合、特に数値計算などを行う列があるときは、あらかじめデータ型を指定しておくと良いでしょう。データ型を指定せずにobject
型のままデータを追加していくと、予期しない型(例えば、数値の列に文字列が混ざってしまうなど)になったり、型推論のコストがかかったりする可能性があります。
dtype
引数を使うと、すべての列に同じデータ型を指定できます。
“`python
import numpy as np
すべての列を整数型 (int64) として空のDataFrameを作成
df_empty_int = pd.DataFrame(columns=[‘ID’, ‘Count’], dtype=np.int64)
print(df_empty_int)
print(df_empty_int.dtypes)
“`
出力例:
Empty DataFrame
Columns: [ID, Count]
Index: []
ID int64
Count int64
dtype: object
注意点: dtype
引数は、columns
で指定したすべての列に同じ型を適用します。もし列ごとに異なる型を指定したい場合は、別の方法をとる必要があります。
1.4. 列ごとに異なるデータ型を指定して空のDataFrameを作成
複数の列があり、それぞれ異なるデータ型を指定したい場合は、辞書を使ってDataFrameを作成する方法が便利です。この辞書の各要素のキーが列名、値がその列のデータを保持するPandas Seriesとなります。空のDataFrameを作る場合は、データを持たない空のSeriesを作成します。空のSeriesは、pd.Series(dtype='データ型')
のようにして作成できます。
“`python
列ごとに異なるデータ型を指定して空のDataFrameを作成
df_empty_typed = pd.DataFrame({
‘ID’: pd.Series(dtype=’int64′), # 整数型
‘Name’: pd.Series(dtype=’object’), # 文字列など (Pythonオブジェクト)
‘Price’: pd.Series(dtype=’float64′), # 浮動小数点型
‘IsValid’: pd.Series(dtype=’bool’) # ブール型
})
print(df_empty_typed)
print(df_empty_typed.dtypes)
“`
出力例:
Empty DataFrame
Columns: [ID, Name, Price, IsValid]
Index: []
ID int64
Name object
Price float64
IsValid bool
dtype: object
この方法を使えば、後からデータを追加する際に、定義した列の型が維持されることが期待できます(ただし、追加するデータの型が定義と一致している必要があります)。特に、数値型の列を定義しておくことは重要です。object
型で始まった列に数値と文字列が混在するような状況を防ぐことができます。
Pandas 1.0以降では、文字列専用の新しいデータ型string
が導入されました。これも指定できます。
“`python
文字列型に ‘string’ dtype を使用する例
df_empty_string_type = pd.DataFrame({
‘ProductID’: pd.Series(dtype=’int64′),
‘ProductName’: pd.Series(dtype=’string’) # string dtype
})
print(df_empty_string_type.dtypes)
“`
出力例:
ProductID int64
ProductName string
dtype: object
1.5. インデックスを指定して空のDataFrameを作成
DataFrameは、各行を一意に識別するためのインデックスを持つことができます。空のDataFrameを作成する際に、将来使用する予定のインデックスをあらかじめ指定することも可能です。これは、特定のキーを持つデータ構造として空のDataFrameを準備する場合などに役立ちます。index
引数にインデックスのリストやインデックスオブジェクトを指定します。
“`python
特定のインデックスを持つ空のDataFrameを作成
custom_index = [‘RowA’, ‘RowB’, ‘RowC’]
df_empty_with_index = pd.DataFrame(columns=[‘Data’], index=custom_index)
print(df_empty_with_index)
print(df_empty_with_index.index)
“`
出力例:
Empty DataFrame
Columns: [Data]
Index: [RowA, RowB, RowC]
Index(['RowA', 'RowB', 'RowC'], dtype='object')
この例では、行データは空ですが、インデックスは3つ定義されています。
1.6. 既存のDataFrameの構造をコピーして空のDataFrameを作成
すでに存在するDataFrameと同じ列名、データ型、およびインデックスを持つ空のDataFrameを作成したい場合があります。これは、元のDataFrameから特定の行だけを抽出した結果を格納する場合などに便利です。
最も簡単な方法は、スライシングを使って「最初の0行」を選択することです。
“`python
既存のDataFrameを作成
df_existing = pd.DataFrame({
‘Category’: [‘A’, ‘B’, ‘A’],
‘Value’: [100, 200, 150],
‘Timestamp’: pd.to_datetime([‘2023-01-01’, ‘2023-01-02’, ‘2023-01-03’])
})
print(“既存のDataFrame:”)
print(df_existing)
print(df_existing.dtypes)
既存のDataFrameの構造をコピーして空のDataFrameを作成 (スライシング)
df_empty_from_existing = df_existing.iloc[0:0]
print(“\n構造をコピーして作成した空のDataFrame:”)
print(df_empty_from_existing)
print(df_empty_from_existing.dtypes) # データ型もコピーされている
“`
出力例:
“`
既存のDataFrame:
Category Value Timestamp
0 A 100 2023-01-01
1 B 200 2023-01-02
2 A 150 2023-01-03
Category object
Value int64
Timestamp datetime64[ns]
dtype: object
構造をコピーして作成した空のDataFrame:
Empty DataFrame
Columns: [Category, Value, Timestamp]
Index: []
Category object
Value int64
Timestamp datetime64[ns]
dtype: object
“`
df_existing.iloc[0:0]
は、「0行目から0行目の直前まで」、つまり行を全く含まないスライスを返します。この結果は、元のDataFrameと同じ列名、列のデータ型、インデックスの種類(ただし、行データがないため具体的なインデックス値は含まれない)を持つ新しいDataFrameとなります。これは非常に効率的で一般的な方法です。
別の方法として、pd.DataFrame
コンストラクタに明示的にcolumns
, index
, dtype
を指定することも可能ですが、iloc[0:0]
がより簡潔です。
“`python
明示的に構造を指定してコピー (iloc[0:0] の代替、やや冗長)
df_empty_from_existing_explicit = pd.DataFrame(
columns=df_existing.columns,
index=[], # もし特定のインデックスが必要なら指定
dtype=df_existing.dtypes # dtypeはすべての列に一括で適用されるわけではない点に注意
)
dtype=df_existing.dtypes は DataFrame コンストラクタでは期待通りに動かないことがあります。
Series ごとに dtype を指定する方法が確実です。
df_empty_from_existing_explicit_correct = pd.DataFrame({col: pd.Series(dtype=df_existing[col].dtype) for col in df_existing.columns})
print(“\n構造を明示的に指定して作成した空のDataFrame (正しい型指定):”)
print(df_empty_from_existing_explicit_correct.dtypes)
“`
iloc[0:0]
が元のデータ型を保持する最も簡単で推奨される方法です。
2. 空のDataFrameの判定
DataFrameが空であるかどうかをプログラムで判定する方法はいくつかあります。状況に応じて最適な方法を選択することが重要です。
2.1. .empty
属性の使用 (最も推奨)
DataFrameが空であるかどうかの判定には、.empty
属性を使用するのが最も簡単で直感的、かつ効率的な方法です。DataFrameにデータ行が全く含まれていない場合にTrue
を返します。列があるかないかに関わらず、行数が0であればTrue
になります。
“`python
空のDataFrameを作成
df_empty = pd.DataFrame()
df_empty_with_cols = pd.DataFrame(columns=[‘ColA’, ‘ColB’])
データを持つDataFrameを作成
df_not_empty = pd.DataFrame({‘ColA’: [1, 2]})
DataFrameの判定
print(f”df_empty.empty: {df_empty.empty}”) # True (行も列もない)
print(f”df_empty_with_cols.empty: {df_empty_with_cols.empty}”) # True (列はあるが行がない)
print(f”df_not_empty.empty: {df_not_empty.empty}”) # False (行がある)
“`
出力:
df_empty.empty: True
df_empty_with_cols.empty: True
df_not_empty.empty: False
.empty
属性は、DataFrameが「行データを持っていないか?」という問いに直接答えるため、ほとんどの場合でこれが最良の判定方法です。
2.2. len()
関数または.shape
属性の使用
DataFrameが持つ行の数は、Pythonの組み込み関数len()
またはDataFrameの.shape
属性の最初の要素(.shape[0]
)で取得できます。これらが0であれば、DataFrameは空であると判定できます。
“`python
df_empty = pd.DataFrame()
df_empty_with_cols = pd.DataFrame(columns=[‘ColA’, ‘ColB’])
df_not_empty = pd.DataFrame({‘ColA’: [1, 2]})
len() で判定
print(f”len(df_empty) == 0: {len(df_empty) == 0}”) # True
print(f”len(df_empty_with_cols) == 0: {len(df_empty_with_cols) == 0}”) # True
print(f”len(df_not_empty) == 0: {len(df_not_empty) == 0}”) # False
.shape[0] で判定
print(f”df_empty.shape[0] == 0: {df_empty.shape[0] == 0}”) # True
print(f”df_empty_with_cols.shape[0] == 0: {df_empty_with_cols.shape[0] == 0}”) # True
print(f”df_not_empty.shape[0] == 0: {df_not_empty.shape[0] == 0}”) # False
“`
出力:
len(df_empty) == 0: True
len(df_empty_with_cols) == 0: True
len(df_not_empty) == 0: False
df_empty.shape[0] == 0: True
df_empty_with_cols.shape[0] == 0: True
df_not_empty.shape[0] == 0: False
len(df)
とdf.shape[0]
はどちらも行数を返すため、行数が0であることの判定には等価です。これらの方法も正確に行数が0であるかを確認できますが、DataFrameが空であるという意図をより明確に伝えるためには.empty
属性の方が優れています。また、内部的な実装として.empty
属性がより効率的である可能性も指摘されています。
2.3. その他の判定方法との比較 (非推奨)
DataFrameが「空」かどうかを判定する際に、isnull().all().all()
のようなメソッドの使用を考える人もいるかもしれません。これは、「DataFrame内のすべての要素がNaN(欠損値)であるか?」を判定する方法です。
“`python
df_all_nan = pd.DataFrame([[np.nan, np.nan], [np.nan, np.nan]])
df_empty = pd.DataFrame()
print(f”df_all_nan:\n{df_all_nan}\n”)
print(f”df_empty:\n{df_empty}\n”)
print(f”df_all_nan.isnull().all().all(): {df_all_nan.isnull().all().all()}”) # True
print(f”df_empty.isnull().all().all(): {df_empty.isnull().all().all()}”) # Error! 空のDataFrameでは実行できない
“`
isnull().all().all()
は、あくまでデータが全てNaNであるかを判定する方法であり、行数が0であるかの判定には使えません。実際、完全に空のDataFrame(行数0)に対しては、この操作自体が実行できない場合や、意図しない結果を返す場合があります。
同様に、DataFrameをイテレートしたり、特定の列にアクセスしようとしたりする前に空であるかを確認したい場合、.empty
属性またはlen()
/.shape
を使うのが適切です。
結論: DataFrameがデータ行を全く含んでいないかを確認する最も推奨される方法は、.empty
属性を使用することです。これは最も意図が明確で、効率的な方法です。
3. なぜ空のDataFrameが必要なのか?(ユースケース)
空のDataFrameがどのような場面で役立つのか、より具体的に掘り下げてみましょう。
3.1. 処理結果を格納するための初期化
これは最も一般的なユースケースの一つです。
例えば、複数のファイルからデータを読み込み、特定の条件に合致するレコードだけを抽出して、最終的に一つのDataFrameにまとめたいとします。最初に結果を格納する空のDataFrameを作成しておき、ループ処理の中で条件に合致したデータをそのDataFrameに追加していく、というパターンです。
“`python
例:複数のデータソースから特定の条件に合致するデータを集める
結果を格納するための空のDataFrameを初期化(列と型を指定しておくと安全)
result_df = pd.DataFrame(columns=[‘SourceFile’, ‘Category’, ‘Value’],
dtype={‘SourceFile’: ‘object’, ‘Category’: ‘object’, ‘Value’: ‘int64’})
file_list = [‘data_part1.csv’, ‘data_part2.csv’, ‘data_part3.csv’]
ダミーファイルの作成 (実際にはファイルから読み込む)
pd.DataFrame({‘Category’: [‘A’, ‘B’], ‘Value’: [10, 20]}).to_csv(‘data_part1.csv’, index=False)
pd.DataFrame({‘Category’: [‘B’, ‘C’], ‘Value’: [30, 40]}).to_csv(‘data_part2.csv’, index=False)
pd.DataFrame({‘Category’: [‘A’, ‘C’], ‘Value’: [50, 60]}).to_csv(‘data_part3.csv’, index=False)
条件に合致するデータ(例:Categoryが’A’のもの)を抽出し、結果DataFrameに追加
for file_name in file_list:
try:
# ファイル読み込み
df_part = pd.read_csv(file_name)
# 条件フィルタリング
filtered_data = df_part[df_part['Category'] == 'A'].copy() # 警告回避のためcopy()
# ソースファイル名を列として追加
filtered_data['SourceFile'] = file_name
# 結果DataFrameに追加 (pd.concatを使用)
# この時点で result_df は空または既存のデータを持つ
# filtered_data は空のDataFrameかもしれない (条件に合致する行がない場合)
# pd.concat は空のDataFrameも適切に扱える
result_df = pd.concat([result_df, filtered_data], ignore_index=True)
except FileNotFoundError:
print(f"Error: {file_name} not found.")
except Exception as e:
print(f"Error processing {file_name}: {e}")
print(“\n最終的な結果DataFrame:”)
print(result_df)
ダミーファイルの削除
import os
for file_name in file_list:
if os.path.exists(file_name):
os.remove(file_name)
“`
この例では、result_df = pd.DataFrame(...)
で空のDataFrameとして初期化しています。ループの中で読み込んだデータから条件に合う行だけを抽出し、pd.concat()
を使ってresult_df
に追加しています。pd.concat()
は、結合対象のDataFrameの一つまたは両方が空であっても正しく機能するため、このパターンに適しています。
3.2. 関数の戻り値として
特定の条件を満たすデータを探す関数を作成する場合、対象データが見つからなかったときに何を返すかという問題があります。エラーを発生させることも可能ですが、呼び出し元で例外処理が必要になります。代わりに空のDataFrameを返すように設計すると、呼び出し元は返されたDataFrameが空であるか(.empty
属性で判定)を確認するだけで済み、処理がシンプルになる場合があります。
“`python
例:特定のIDを持つ顧客データを検索する関数
def get_customer_data(customer_id: int) -> pd.DataFrame:
“””
顧客IDに対応するデータを検索し、DataFrameで返す。
存在しない場合は空のDataFrameを返す。
“””
# 仮の顧客データ
all_customers = pd.DataFrame({
‘ID’: [101, 102, 103, 104],
‘Name’: [‘Alice’, ‘Bob’, ‘Charlie’, ‘David’],
‘City’: [‘Tokyo’, ‘Osaka’, ‘Tokyo’, ‘Fukuoka’]
})
# IDでフィルタリング
result = all_customers[all_customers['ID'] == customer_id]
# 結果が空かどうかに関わらずDataFrameを返す
# もし結果が空の場合、自動的に空のDataFrameになる
return result
関数呼び出しと結果の判定
customer_102_df = get_customer_data(102)
customer_999_df = get_customer_data(999) # 存在しないID
print(“ID 102 のデータ:”)
if customer_102_df.empty:
print(“データは見つかりませんでした。”)
else:
print(customer_102_df)
print(“\nID 999 のデータ:”)
if customer_999_df.empty:
print(“データは見つかりませんでした。”)
else:
print(customer_999_df)
“`
出力例:
“`
ID 102 のデータ:
ID Name City
1 102 Bob Osaka
ID 999 のデータ:
データは見つかりませんでした。
“`
この例では、get_customer_data
関数は、データが見つかっても見つからなくても常にDataFrameを返します。呼び出し元は返されたDataFrameの.empty
属性をチェックするだけで、データが存在したかどうかの判定が可能です。これは、後続の処理がDataFrameを受け取ることを前提としている場合に、エラー処理よりも自然なフローとなることが多いです。
3.3. 構造のテンプレートとしての利用
特定の構造(列名とデータ型)を持つDataFrameが必要だが、まだデータがない場合に、空のDataFrameをテンプレートとして作成しておくことができます。これは、後から異なるソースからデータを取得して、この定義された構造に合うように整形しながら追加していくようなシナリオで役立ちます。
“`python
例:後からデータを投入するための構造テンプレートを作成
注文データを格納するDataFrameの構造を定義
IDは整数、商品名は文字列、数量は整数、価格は浮動小数点
order_template_df = pd.DataFrame({
‘OrderID’: pd.Series(dtype=’int64′),
‘ProductName’: pd.Series(dtype=’string’),
‘Quantity’: pd.Series(dtype=’int64′),
‘Price’: pd.Series(dtype=’float64′)
})
print(“注文データ テンプレートの構造:”)
print(order_template_df.dtypes)
仮の新しい注文データ (例えばAPIから取得したJSON)
new_orders_data = [
{‘OrderID’: 1, ‘ProductName’: ‘Laptop’, ‘Quantity’: 1, ‘Price’: 1200.0},
{‘OrderID’: 2, ‘ProductName’: ‘Mouse’, ‘Quantity’: 5, ‘Price’: 25.5}
]
新しいデータをテンプレート構造のDataFrameに追加
最初に空のテンプレートを使う必要はないが、構造を明確にする役割がある
この例では、テンプレート自体にデータは追加せず、新しいDataFrameを作成して結合
new_orders_df = pd.DataFrame(new_orders_data)
構造が一致することを確認したり、必要なら型変換を行ったりする
(ここでは簡単のため、concatで結合)
concatはデフォルトで共通の列を結合し、存在しない列はNaNで埋める
テンプレートとして厳密に使うなら、new_orders_dfの構造を整形してから結合する方が良い場合もある
例: new_orders_df = new_orders_df[order_template_df.columns].astype(order_template_df.dtypes)
ただし、astype はNaNを扱える dtype でないとエラーになる可能性あり
ここではシンプルに結合の例
final_orders_df = pd.concat([order_template_df, new_orders_df], ignore_index=True)
print(“\nデータ追加後のDataFrame:”)
print(final_orders_df)
print(final_orders_df.dtypes)
“`
出力例:
“`
注文データ テンプレートの構造:
OrderID int64
ProductName string
Quantity int64
Price float64
dtype: object
データ追加後のDataFrame:
OrderID ProductName Quantity Price
0 1 Laptop 1 1200.0
1 2 Mouse 5 25.5
OrderID int64
ProductName string
Quantity int64
Price float64
dtype: object
“`
このユースケースでは、空のDataFrame自体にデータを追加するというよりも、期待するDataFrameの構造をコード上で明確に定義するという役割が大きいです。後から取得するデータがこの構造に合うように変換・結合することで、コードの可読性や保守性を高めることができます。
3.4. 条件分岐の判定基準として
DataFrameが空であるかどうかによって、後続の処理を分岐させたい場合があります。例えば、データが存在する場合のみ集計処理を行う、といったシナリオです。
“`python
例:データが存在する場合のみ処理を実行
def process_data(df: pd.DataFrame):
“””
DataFrameを受け取り、空でなければ集計処理を行う関数。
“””
print(“\nデータ処理関数が呼ばれました。”)
if df.empty:
print(“入力DataFrameは空です。処理をスキップします。”)
else:
print(“入力DataFrameにデータがあります。処理を実行します。”)
# 集計処理の例
summary = df.groupby(‘Category’)[‘Value’].sum()
print(“集計結果:”)
print(summary)
テスト用のDataFrame
data1 = pd.DataFrame({‘Category’: [‘A’, ‘B’], ‘Value’: [100, 200]})
data2 = pd.DataFrame(columns=[‘Category’, ‘Value’]) # 空のDataFrame
process_data(data1)
process_data(data2)
“`
出力例:
“`
データ処理関数が呼ばれました。
入力DataFrameにデータがあります。処理を実行します。
集計結果:
Category
A 100
B 200
Name: Value, dtype: int64
データ処理関数が呼ばれました。
入力DataFrameは空です。処理をスキップします。
“`
このように、関数の先頭で空のDataFrameでないかを確認し、処理をスキップすることで、後続の処理内でエラーが発生するのを防いだり、無駄な計算を省いたりすることができます。これは、.empty
属性の最も一般的で効果的な使い方のひとつです。
4. 空のDataFrameの取り扱い
空のDataFrameを作成した後、それにデータを追加したり、何らかの操作を試みたりすることになります。ここでは、空のDataFrameに対する一般的な操作とその注意点について解説します。
4.1. データ(行)の追加方法
空のDataFrameにデータ(新しい行)を追加する方法はいくつかありますが、最も推奨されるのはpd.concat()
関数を使用することです。過去には.append()
メソッドもよく使われましたが、これは現在廃止予定(deprecated)であり、代わりにpd.concat()
を使うことが推奨されています。
推奨される方法: pd.concat()
pd.concat()
は、複数のDataFrameやSeriesを連結するための関数です。空のDataFrameに新しいデータを追加する場合、元の空のDataFrameと追加したいデータを保持するDataFrame(またはSeries)をリストとしてpd.concat()
に渡します。
“`python
結果を格納するための空のDataFrameを初期化
result_df = pd.DataFrame(columns=[‘ID’, ‘Name’, ‘Score’],
dtype={‘ID’: ‘int64’, ‘Name’: ‘object’, ‘Score’: ‘float64’})
追加するデータ(単一行の場合)
new_row_data_dict = {‘ID’: 1, ‘Name’: ‘Alice’, ‘Score’: 95.5}
辞書をDataFrameに変換(pd.concatはDataFrameのリストを受け取るため)
new_row_df = pd.DataFrame([new_row_data_dict]) # リストで囲むことで1行のDataFrameになる
空のDataFrameに単一行を追加
result_df = pd.concat([result_df, new_row_df], ignore_index=True)
print(“1行追加後:”)
print(result_df)
print(result_df.dtypes)
追加するデータ(複数行の場合)
more_data_list_of_dicts = [
{‘ID’: 2, ‘Name’: ‘Bob’, ‘Score’: 88.0},
{‘ID’: 3, ‘Name’: ‘Charlie’, ‘Score’: 76.5}
]
more_data_df = pd.DataFrame(more_data_list_of_dicts)
既存のDataFrameに複数行を追加
result_df = pd.concat([result_df, more_data_df], ignore_index=True)
print(“\nさらに複数行追加後:”)
print(result_df)
print(result_df.dtypes)
“`
出力例:
“`
1行追加後:
ID Name Score
0 1 Alice 95.5
ID int64
Name object
Score float64
dtype: object
さらに複数行追加後:
ID Name Score
0 1 Alice 95.5
1 2 Bob 88.0
2 3 Charlie 76.5
ID int64
Name object
Score float64
dtype: object
“`
pd.concat()
を使う際の重要な引数:
ignore_index=True
: 結合後のDataFrameで新しい連番のインデックスを生成します。元のDataFrameや追加するDataFrameのインデックスを引き継がず、単純な連番にしたい場合に使います。ループ処理でデータを追加していく際など、元のインデックスに意味がない場合は通常True
を指定します。ignore_index=False
(デフォルト): 元のDataFrameと追加するDataFrameのインデックスをそのまま使用します。インデックスが重複する可能性があります。
pd.concat
は、空のDataFrameと非空のDataFrameを結合した場合でも、あるいは両方が空の場合でも正しく機能します。また、列名が一致しない場合は、一致しない列は結果に含めつつ、データがない部分にはNaNが挿入されます。初期の空のDataFrameを作成する際に列名と型を正確に定義しておくことで、このような予期しないNaNの混入を防ぎやすくなります。
非推奨の方法: .loc
による行追加
DataFrameの.loc
インデクサーを使って、存在しないインデックスを指定して新しい行を直接追加することも可能ではありますが、推奨されません。特にループ処理などで繰り返し1行ずつ追加する場合、非常に非効率的になる可能性があります。Pandasは内部的にデータを効率的に管理していますが、.loc
での逐次的な行追加は、データ構造の再構築を頻繁に引き起こし、パフォーマンスが著しく低下するためです。
“`python
非推奨な方法: .loc で行を追加する例
初期化は上記と同じでも良いが、ここでは列名だけ指定
result_df_loc = pd.DataFrame(columns=[‘ID’, ‘Name’, ‘Score’]) # dtypeを指定しない場合 object になる
追加するデータ
new_data_list = [
{‘ID’: 1, ‘Name’: ‘Alice’, ‘Score’: 95.5},
{‘ID’: 2, ‘Name’: ‘Bob’, ‘Score’: 88.0},
{‘ID’: 3, ‘Name’: ‘Charlie’, ‘Score’: 76.5}
]
ループで1行ずつ .loc で追加
パフォーマンス上の問題があるため、実際のコードでは避けるべき
for i, row_data in enumerate(new_data_list):
result_df_loc.loc[i] = row_data # インデックスを指定して行追加
print(“\n.loc で行追加後のDataFrame (非推奨):”)
print(result_df_loc)
print(result_df_loc.dtypes) # dtype が object になっている可能性が高い
“`
出力例:
.loc で行追加後のDataFrame (非推奨):
ID Name Score
0 1 Alice 95.5
1 2 Bob 88.0
2 3 Charlie 76.5
ID object
Name object
Score object
dtype: object
この方法で追加した場合、特に初期の空のDataFrameで型を指定していないと、object
型になりがちです。後から型変換が必要になる可能性もあります。
非推奨/廃止予定の方法: .append()
DataFrameの.append()
メソッドも行を追加するために使われていましたが、Pandasの将来のバージョンで廃止される予定です。代わりにpd.concat()
を使用することが推奨されています。
“`python
廃止予定の方法: .append() で行を追加する例
初期化
result_df_append = pd.DataFrame(columns=[‘ID’, ‘Name’])
追加するデータ
new_row_series = pd.Series({‘ID’: 1, ‘Name’: ‘Alice’})
1行追加 (append は新しいDataFrameを返す)
この方法も内部的には concat と似た処理になるが、推奨されない
result_df_append = result_df_append.append(new_row_series, ignore_index=True) # 警告が出る
print(“\n.append で行追加後のDataFrame (廃止予定):”)
print(result_df_append)
“`
まとめ: 空のDataFrameにデータを追加する際は、必ずpd.concat()
を使用し、特にループ処理でデータを集める場合は、追加したいデータを一度リストに格納しておき、ループの最後にまとめてpd.concat()
するのが最も効率的なパターンです(後述のパフォーマンス考慮事項を参照)。
4.2. 空のDataFrameに対するその他の操作
空のDataFrameに対しても、多くのDataFrame操作を試みることができます。結果は通常、空のDataFrameになりますが、エラーになる場合もあります。
- 列の追加/削除: 空であっても列を追加したり削除したりできます。
python
df_empty_cols = pd.DataFrame(columns=['A', 'B'])
df_empty_cols['C'] = [] # 新しい列を追加 (データは当然空)
print(df_empty_cols)
del df_empty_cols['A'] # 列を削除
print(df_empty_cols) - 型の変更:
astype()
を使って列の型を変更することも可能ですが、データがないため実際に値が変換されるわけではありません。テンプレートとして型を指定し直すようなイメージです。
python
df_empty_obj = pd.DataFrame(columns=['Value'])
print(df_empty_obj.dtypes)
df_empty_float = df_empty_obj.astype({'Value': 'float64'})
print(df_empty_float.dtypes) - フィルタリング: 条件を指定しても、データがないため結果は常に空のDataFrameになります。
python
df_empty_filtered = pd.DataFrame(columns=['Value'])
filtered = df_empty_filtered[df_empty_filtered['Value'] > 100]
print(filtered) # Empty DataFrame -
集計 (
groupby
,sum
,mean
など): 集計操作を試みた場合、データがないため通常は空のSeriesやDataFrameが返されます。ただし、操作によってはエラーになることもあります(例:mean()
など、データが存在しないと計算できない場合)。
“`python
df_empty_agg = pd.DataFrame(columns=[‘Category’, ‘Value’], dtype=’object’)
grouped = df_empty_agg.groupby(‘Category’)[‘Value’].sum()
print(grouped) # Empty Series or DataFrame depending on pandas version and keysエラーになりうる例 (空のデータに対する統計量計算)
try:
print(df_empty_agg[‘Value’].mean())
except Exception as e:
print(f”Mean calculation on empty data failed: {e}”)
- **結合 (`merge`, `join`):** 空のDataFrameと別のDataFrameを結合した場合、結果は通常空のDataFrameになります。
python
df_empty_merge = pd.DataFrame(columns=[‘ID’, ‘Name’])
df_other = pd.DataFrame({‘ID’: [1, 2], ‘City’: [‘A’, ‘B’]})
merged = pd.merge(df_empty_merge, df_other, on=’ID’, how=’left’)
print(merged) # Empty DataFrame
“`
空のDataFrameに対して操作を行う場合、結果が空になることを想定しておけば問題ありません。重要なのは、操作によってはエラーが発生する可能性があることを理解しておくことです。特にデータ集計関数を使う前には、.empty
でDataFrameが空でないかを確認するガード節を入れることが推奨されます(前述のユースケース3.4参照)。
4.3. データ型推論と明示的な型指定の重要性
空のDataFrameに後からデータを追加していく場合、初期の空のDataFrameでデータ型を適切に指定しておくことが非常に重要です。
もしpd.DataFrame(columns=[...])
のように列名だけを指定して作成した場合、列の型はデフォルトでobject
になります。
python
df_initial_object = pd.DataFrame(columns=['ID', 'Value'])
print(df_initial_object.dtypes)
出力:
ID object
Value object
dtype: object
このDataFrameに、例えば整数や浮動小数点のデータを追加したとします。
python
data_to_add = pd.DataFrame([{'ID': 1, 'Value': 10.5}, {'ID': 2, 'Value': 20}])
df_combined = pd.concat([df_initial_object, data_to_add], ignore_index=True)
print(df_combined)
print(df_combined.dtypes)
出力例:
ID Value
0 1 10.5
1 2 20.0
ID int64
Value float64
dtype: object
この場合、Pandasは追加されたデータの型を推論し、結合後のDataFrameでは適切な型(IDはint64
、Valueはfloat64
)になりました。多くの場合はこれで問題ありません。
しかし、以下のようなケースでは問題が起こり得ます。
- 意図しない型の混在: もし
object
型の列に、異なる型のデータが混在して追加される可能性がある場合(例: 数値と文字列)、その列はobject
型のままになり、後続の数値計算などでエラーが発生する可能性があります。 - 型推論のコスト: 大量のデータを
object
型の列に追加する場合、Pandasが都度型推論を行うためのオーバーヘッドが発生する可能性があります。 - 欠損値 (NaN) の扱い: 数値型やブール型ではない
object
型の場合、欠損値はNaNではなくNone
として扱われることがあり、欠損値処理の挙動が異なります。また、数値型の列をobject
型として作成した場合、後からNaNを追加すると列全体がobject
型に「格下げ」されることがあります。
したがって、後から追加されるデータの型が分かっている場合は、初期の空のDataFrameを作成する際に、列ごとに明示的に型を指定しておくことを強く推奨します。これは前述の「1.4. 列ごとに異なるデータ型を指定して空のDataFrameを作成」で示した方法です。
“`python
明示的に型を指定して空のDataFrameを作成
df_initial_typed = pd.DataFrame({
‘ID’: pd.Series(dtype=’int64′),
‘Value’: pd.Series(dtype=’float64′)
})
print(df_initial_typed.dtypes)
同じデータを追加
data_to_add = pd.DataFrame([{‘ID’: 1, ‘Value’: 10.5}, {‘ID’: 2, ‘Value’: 20}])
df_combined_typed = pd.concat([df_initial_typed, data_to_add], ignore_index=True)
print(df_combined_typed.dtypes) # 型が維持されている
“`
出力例:
ID int64
Value float64
dtype: object
ID int64
Value float64
dtype: object
このように、初期段階で型を定義しておくことで、コードの意図が明確になり、データ追加時の挙動が安定し、潜在的な型関連のエラーを防ぐことができます。
5. 実例:空のDataFrameを使った典型的な処理フロー
これまでに学んだ空のDataFrameの作成、判定、およびデータ追加の知識を使って、より具体的なデータ処理のシナリオを実装してみましょう。
実例1:複数のCSVファイルを読み込み、特定の条件を満たす行だけを集計する
これは前述のユースケース3.1をさらに掘り下げた例です。多数のCSVファイルがあり、それぞれのファイルから特定の条件(例: ある列の値が特定の範囲内)を満たす行だけを抽出し、それらすべてを結合した上で、最終的な集計や分析を行うケースです。
“`python
実例1: 複数ファイルからのデータ集計
ダミーCSVファイルの作成
data_dir = ‘temp_csv_data’
os.makedirs(data_dir, exist_ok=True)
pd.DataFrame({‘Date’: [‘2023-01-01’, ‘2023-01-02’], ‘Category’: [‘A’, ‘B’], ‘Sales’: [100, 150]}).to_csv(os.path.join(data_dir, ‘sales_202301.csv’), index=False)
pd.DataFrame({‘Date’: [‘2023-02-01’, ‘2023-02-03’], ‘Category’: [‘B’, ‘A’], ‘Sales’: [200, 120]}).to_csv(os.path.join(data_dir, ‘sales_202302.csv’), index=False)
pd.DataFrame({‘Date’: [‘2023-03-05’, ‘2023-03-10’], ‘Category’: [‘C’, ‘B’], ‘Sales’: [50, 180]}).to_csv(os.path.join(data_dir, ‘sales_202303.csv’), index=False)
存在しないファイルもリストに含めてエラー処理を試す
file_list = [‘sales_202301.csv’, ‘sales_202302.csv’, ‘sales_202303.csv’, ‘non_existent_file.csv’]
file_list = [‘sales_202301.csv’, ‘sales_202302.csv’, ‘sales_202303.csv’]
結果を格納するための空のDataFrameを初期化
後から結合することを考慮し、列名とデータ型を事前に定義
Sales列をfloat64にすることで、欠損値(NaN)が入っても対応しやすくする
all_sales_data = pd.DataFrame({
‘FileName’: pd.Series(dtype=’object’),
‘Date’: pd.Series(dtype=’datetime64[ns]’),
‘Category’: pd.Series(dtype=’object’),
‘Sales’: pd.Series(dtype=’float64′)
})
データを一時的に保持するリスト (concatを効率的に行うため)
data_parts = []
print(“— ファイル読み込みとフィルタリング —“)
for file_name in file_list:
file_path = os.path.join(data_dir, file_name)
print(f”処理中: {file_name}”)
try:
# ファイル読み込み
df_file = pd.read_csv(file_path)
df_file[‘Date’] = pd.to_datetime(df_file[‘Date’]) # 日付型に変換
# 条件フィルタリング: 例として、Salesが150以上の行を抽出
filtered_df = df_file[df_file['Sales'] >= 150].copy()
# もしフィルタリングでデータが残った場合のみ処理
if not filtered_df.empty:
print(f" -> {len(filtered_df)} 件のデータが見つかりました。")
# 元のファイル名を識別できるように列として追加
filtered_df['FileName'] = file_name
# 結果リストに追加
# 列の順序や型を初期DataFrameに合わせる (optional but good practice)
# filtered_df = filtered_df[all_sales_data.columns].astype(all_sales_data.dtypes)
# ただし astype は NaN の扱いに注意が必要
# ここではシンプルに、concat に任せる
data_parts.append(filtered_df)
else:
print(" -> 条件に合致するデータはありませんでした。")
except FileNotFoundError:
print(f"エラー: {file_name} が見つかりませんでした。")
except pd.errors.EmptyDataError:
print(f"警告: {file_name} は空のファイルです。スキップします。")
except Exception as e:
print(f"エラー発生 ({file_name}): {e}")
print(“\n— データの結合 —“)
集めたデータ parçalarını tek bir DataFrame’e birleştir
if data_parts: # data_parts リストが空でない場合のみ concat を実行
# リストに蓄積したDataFrameをまとめて concat
all_sales_data = pd.concat([all_sales_data] + data_parts, ignore_index=True)
# 注意: all_sales_data = pd.concat(data_parts, ignore_index=True) でも良い場合が多いが
# 最初の空の DataFrame を含めることで、例え data_parts が空でも最終結果が空の DataFrame になることを保証できる
# また、初期 DataFrame で指定した列順序や型が維持されやすくなる(完全に保証ではない)
else:
# data_parts が空の場合、all_sales_data は初期化時の空の DataFrame のまま
print(“すべてのファイルで条件に合致するデータはありませんでした。”)
print(“\n— 最終結果DataFrame —“)
print(all_sales_data)
print(“\n最終結果DataFrameの型:”)
print(all_sales_data.dtypes)
print(“\n— 最終的な集計 —“)
if not all_sales_data.empty:
# 集計処理 例:Categoryごとの合計売上
category_summary = all_sales_data.groupby(‘Category’)[‘Sales’].sum()
print(“Category別合計売上:”)
print(category_summary)
else:
print(“集計するデータがありません。”)
ダミーディレクトリとファイルの削除
import shutil
if os.path.exists(data_dir):
shutil.rmtree(data_dir)
“`
この例では、まずall_sales_data
という空のDataFrameを初期化しています。ループ内でファイルを読み込み、条件に合うデータを抽出します。抽出結果が空でない場合、そのDataFrameをdata_parts
というリストに追加していきます。ループの終了後、pd.concat([all_sales_data] + data_parts, ignore_index=True)
という形で、初期の空のDataFrameと集めたDataFrameのリストをまとめて結合しています。これにより、データが全く見つからなかった場合でも、最終的なall_sales_data
は初期化時と同じ構造の空のDataFrameとなり、その後の.empty
判定や集計処理がエラーなく行えます。リストに溜めてからまとめてconcat
する方が、ループ内で繰り返しconcat
するよりも一般的に効率が良いです。
実例2:外部APIからデータを取得し、構造化してDataFrameに格納する
外部APIからデータを取得する際、レスポンスはJSON形式などで返されることが多いでしょう。これらのデータをDataFrameとして扱うために、あらかじめDataFrameの構造(列名と型)を定義しておき、取得したデータを整形しながら追加していくパターンです。APIからのレスポンスがリスト形式の場合、比較的容易にDataFrameに変換できますが、データが単一のオブジェクトや複雑な構造の場合、少し工夫が必要になります。空のDataFrameをテンプレートとして使うと、このような整形・追加処理がやりやすくなります。
“`python
実例2: 外部APIからのデータをDataFrameに格納
想定されるAPIレスポンスの構造に基づき、空のDataFrameをテンプレートとして定義
Product ID (int), Name (string), Price (float), Available Stock (int)
product_data_template = pd.DataFrame({
‘ProductID’: pd.Series(dtype=’int64′),
‘ProductName’: pd.Series(dtype=’string’), # string dtype を使用
‘Price’: pd.Series(dtype=’float64′),
‘Stock’: pd.Series(dtype=’int64′)
})
API呼び出しの結果を格納するリスト
api_data_list = []
仮のAPIレスポンスデータ (通常は requests ライブラリなどで取得)
レスポンスの形式が異なる場合をシミュレート
api_responses = [
{‘id’: 101, ‘name’: ‘Laptop’, ‘price’: 1200.50, ‘stock’: 50}, # 正常データ
{‘id’: 102, ‘name’: ‘Mouse’, ‘price’: 25.99}, # 一部列が欠損
{‘id’: 103, ‘product_name’: ‘Keyboard’, ‘cost’: 75.0}, # 列名が異なる
{} # データが空のオブジェクト
# {‘error’: ‘item not found’} # エラーレスポンスなども考えられる
]
print(“— APIレスポンス処理 —“)
for i, response in enumerate(api_responses):
print(f”処理中 レスポンス {i+1}: {response}”)
# レスポンスが有効なデータを含んでいるかチェック
# (実際にはAPIのエラーコードなども確認する必要がある)
if response and ‘id’ in response: # 例: ‘id’ キーが存在すれば有効とみなす
# APIレスポンスのキーをテンプレートの列名にマッピングし、必要なデータだけ抽出
processed_data = {
'ProductID': response.get('id'),
'ProductName': response.get('name') or response.get('product_name'), # name または product_name を ProductName に
'Price': response.get('price') or response.get('cost'), # price または cost を Price に
'Stock': response.get('stock', 0) # stock がなければ 0 とする
}
# 辞書を1行のDataFrameに変換
processed_df = pd.DataFrame([processed_data])
# テンプレートに合わせて列の順番を並べ替える (任意だが推奨)
# テンプレートにない列は除外される
processed_df = processed_df[product_data_template.columns]
# リストに追加
api_data_list.append(processed_df)
print(" -> データ抽出成功")
else:
print(" -> 有効なデータ形式ではありませんでした。スキップします。")
print(“\n— データの結合 —“)
集めたデータを単一のDataFrameに結合
if api_data_list:
# 空のテンプレートと集めたデータを結合
final_product_df = pd.concat([product_data_template] + api_data_list, ignore_index=True)
else:
# データが全く集まらなかった場合
final_product_df = product_data_template # 初期テンプレートを最終結果とする
print(“\n— 最終結果DataFrame —“)
print(final_product_df)
print(“\n最終結果DataFrameの型:”)
print(final_product_df.dtypes)
確認: 欠損した列はNaN、型は維持されているか
Stockが欠損していた行は、テンプレートのdtype(‘int64’)によりNaNが入るが、int64はNaNを表現できないため float64 になる(Pandasの挙動)
これを避けるためには、欠損を許容する型(Int64, Float64など)を使うか、事前に欠損を埋める必要がある
ProductName は string dtype なので、欠損は となる
“`
出力例:
“`
— APIレスポンス処理 —
処理中 レスポンス 1: {‘id’: 101, ‘name’: ‘Laptop’, ‘price’: 1200.5, ‘stock’: 50}
-> データ抽出成功
処理中 レスポンス 2: {‘id’: 102, ‘name’: ‘Mouse’, ‘price’: 25.99}
-> データ抽出成功
処理中 レスポンス 3: {‘product_name’: ‘Keyboard’, ‘cost’: 75.0}
-> 有効なデータ形式ではありませんでした。スキップします。
処理中 レスポンス 4: {}
-> 有効なデータ形式ではありませんでした。スキップします。
— データの結合 —
— 最終結果DataFrame —
ProductID ProductName Price Stock
0 101 Laptop 1200.50 50.0
1 102 Mouse 25.99 NaN
最終結果DataFrameの型:
ProductID int64
ProductName string
Price float64
Stock float64
dtype: object
“`
この例では、product_data_template
という空のDataFrameが、後から追加されるデータの「設計図」として機能しています。APIからのレスポンスごとに、必要な情報を抽出し、テンプレートの列名に合うように整形します。そして、整形済みのデータを一時リストapi_data_list
に追加します。最後に、リストに集めたデータをまとめてテンプレートと結合することで、期待する構造と型を持つDataFrameを効率的に構築しています。一部欠損がある場合の型の挙動(int64列にNaNが入るとfloat64になるなど)はPandasの設計に依存するため、注意が必要です。Pandas 1.0以降で導入されたNull許容整数型(Int64
)などを使用すると、整数列にNaNを保持したままint
型として扱えます。
6. 空のDataFrameと似ているが異なる概念
Pandasを扱う上で、空のDataFrameと混同しやすい他の「空」の状態やオブジェクトがあります。
-
None
: Pythonの組み込みオブジェクトで、「何もない」ことを示します。Pandas DataFrame自体がNone
になることは、DataFrameオブジェクトが生成されなかったことを意味します。空のDataFrameオブジェクトは存在しますが、行データがない状態です。これは全く異なります。
“`python
df_none = None
df_empty = pd.DataFrame()print(f”df_none is None: {df_none is None}”) # True
print(f”df_empty is None: {df_empty is None}”) # False
print(f”df_empty.empty: {df_empty.empty}”) # True
- **空のリスト `[]` や 空の辞書 `{}`:** これらはPandasのデータ構造ではなく、Pythonの基本的なコンテナ型です。DataFrameはこれらのコンテナとは根本的に異なるデータ構造と機能を持っています。
python
empty_list = []
empty_dict = {}
df_empty = pd.DataFrame()print(type(empty_list)) #
print(type(empty_dict)) #
print(type(df_empty)) #
- **列はあるが行がないDataFrame vs 行も列もないDataFrame:** 厳密にはどちらも`.empty`は`True`ですが、その構造は異なります。
python
df_no_rows_no_cols = pd.DataFrame() # shape (0, 0)
df_no_rows_with_cols = pd.DataFrame(columns=[‘A’, ‘B’]) # shape (0, 2)print(f”df_no_rows_no_cols.empty: {df_no_rows_no_cols.empty}”) # True
print(f”df_no_rows_with_cols.empty: {df_no_rows_with_cols.empty}”) # Trueprint(f”df_no_rows_no_cols.shape: {df_no_rows_no_cols.shape}”) # (0, 0)
print(f”df_no_rows_with_cols.shape: {df_no_rows_with_cols.shape}”) # (0, 2)
``
.dtypes`を確認したり、列名を操作したりする際には、列があるかないかが重要になります。列だけを持つ空のDataFrameは、テンプレートとしての役割がより明確になります。
多くの場面ではこの違いを意識する必要はありませんが、例えば
7. パフォーマンスに関する考慮事項
空のDataFrameにデータを追加する際のパフォーマンスについて、特にループ処理における注意点があります。
前述の「4.1. データ(行)の追加方法」で触れたように、ループの中でpd.concat()
や.loc
を使って繰り返し単一行を追加していくのは、非効率的です。PandasのDataFrameはイミュータブル(不変)に近い性質を持っており、変更を加えるたびに内部的に新しいオブジェクトが生成されるコストがかかるためです。
より効率的な方法は、追加したいデータを一旦Pythonのリストに格納しておき、ループ処理の最後にそのリストをまとめてpd.concat()
またはpd.DataFrame()
のコンストラクタに渡して一つのDataFrameを生成することです。
“`python
import time
非効率な方法: ループ内で繰り返し concat
print(“— 非効率な concat (ループ内) —“)
start_time = time.time()
result_df_inefficient = pd.DataFrame(columns=[‘ID’, ‘Value’])
for i in range(10000): # 1万行追加
new_row = pd.DataFrame([{‘ID’: i, ‘Value’: i * 1.1}])
result_df_inefficient = pd.concat([result_df_inefficient, new_row], ignore_index=True)
end_time = time.time()
print(f”処理時間: {end_time – start_time:.4f}秒”)
print(result_df_inefficient.tail())
効率的な方法: リストに溜めて最後に concat
print(“\n— 効率的な concat (リスト利用) —“)
start_time = time.time()
data_list_efficient = []
for i in range(10000): # 1万行追加
data_list_efficient.append({‘ID’: i, ‘Value’: i * 1.1})
リストからまとめて DataFrame を作成 または concat
result_df_efficient = pd.DataFrame(data_list_efficient) # リストの辞書から直接 DataFrame を作るのが最も効率的
result_df_efficient = pd.concat(data_list_efficient, ignore_index=True) # リストの辞書を直接concatすることも可能 (Pandas >= 1.4)
または、リストの DataFrame を concat
initial_empty = pd.DataFrame(columns=[‘ID’, ‘Value’])
list_of_dfs = [pd.DataFrame([d]) for d in data_list_efficient]
result_df_efficient = pd.concat([initial_empty] + list_of_dfs, ignore_index=True)
end_time = time.time()
print(f”処理時間: {end_time – start_time:.4f}秒”)
print(result_df_efficient.tail())
リストの辞書から直接 DataFrame を作る方法も試す (最も一般的)
print(“\n— 最も効率的な方法 (リストの辞書から直接 DataFrame) —“)
start_time = time.time()
data_list_most_efficient = []
for i in range(10000):
data_list_most_efficient.append({‘ID’: i, ‘Value’: i * 1.1})
result_df_most_efficient = pd.DataFrame(data_list_most_efficient)
end_time = time.time()
print(f”処理時間: {end_time – start_time:.4f}秒”)
print(result_df_most_efficient.tail())
“`
実行環境によりますが、効率的な方法が非効率な方法よりも圧倒的に高速であることが確認できるはずです。
推奨パターン:
空のDataFrameを初期化し、ループ内でデータを収集する場合は、
- 空のPythonリストを準備する。
- ループの中で、追加したいデータを辞書やSeries、または1行のDataFrameとして作成し、そのリストに追加する。
- ループ終了後、
pd.DataFrame(list_of_dicts)
またはpd.concat(list_of_dfs, ignore_index=True)
を使って、リストの内容をまとめて一つのDataFrameに変換する。
初期の空のDataFrameを結果変数として使い回す場合は、pd.concat([result_df, new_data_df], ignore_index=True)
のように、結合元のリストに含めて繰り返し結合するのではなく、結果を保持する変数自体をリストに溜めたDataFrameで置き換えるようにします。ただし、データが全く見つからなかった場合に備えて、最後に初期の空DataFrameまたはテンプレートと結合する、あるいは最後にリストが空かどうかを判定して初期DataFrameを結果とする、といった工夫が必要です(実例1参照)。
最もクリーンなのは、「結果をリストに溜めて最後にDataFrame化する」パターンです。空のDataFrameは初期テンプレートとしてのみ定義し、結果の蓄積には直接使わない方が、パフォーマンス面では有利なことが多いです。
8. まとめ
この記事では、Pandasにおける空のDataFrameについて、その作成方法から判定、多様なユースケース、具体的な取り扱い方法、そしてパフォーマンスの考慮事項に至るまで、徹底的に解説しました。
- 作成:
pd.DataFrame()
を基本に、columns
、dtype
、辞書形式、既存DataFrameからの構造コピーなど、様々な方法で空のDataFrameを作成できます。目的(特に列名やデータ型)に合わせて適切な方法を選ぶことが重要です。列ごとに異なる型を指定する場合は、辞書形式で空のSeriesを指定する方法が有効です。 - 判定: DataFrameが空であるかどうかの判定には、
.empty
属性を使用するのが最も推奨される方法です。len(df) == 0
やdf.shape[0] == 0
も同等の判定を行えますが、.empty
の方がコードの意図が明確になります。 - ユースケース: 処理結果の初期化、関数の戻り値としての利用、構造テンプレートとしての定義、条件分岐の判定基準など、空のDataFrameは様々なプログラミングパターンで役立ちます。
- 取り扱い: 空のDataFrameにデータを追加する際は、廃止予定の
.append()
や非効率な.loc
の使用を避け、pd.concat()
関数を使用するのが推奨されます。特にループ処理でデータを集める場合は、一旦リストに溜めてからまとめてconcat
またはpd.DataFrame()
で変換するのが最も効率的です。また、後続のデータ追加や操作のために、初期の空のDataFrameで列名とデータ型を適切に定義しておくことが重要です。 - パフォーマンス: ループ内でのDataFrameの繰り返し変更は避けるべきです。データをリストに集めて最後にまとめて処理する方法が効率的です。
空のDataFrameは、それ自体にデータがないため最初は分かりづらいかもしれませんが、データ処理のフローを設計する上で非常に便利な「箱」や「テンプレート」として機能します。この記事で解説した内容を理解し活用することで、Pandasを使ったデータ処理コードをより柔軟に、効率的に、そして堅牢に記述できるようになるでしょう。
データ処理の現場で、空のDataFrameがあなたの強力な味方となることを願っています。