本記事は、Pandas の公式ドキュメントのUser Guide - Categorical dataを機械翻訳した後、一部の不自然な文章を手直ししたものである。
誤訳の指摘・代訳案・質問等があればコメント欄や編集リクエストでお願いします。
Pandas公式ドキュメント日本語訳記事一覧
- データの取得と選択
- マルチインデックス・高度な索引
- mergeとjoinとconcatenateとcompare
- テーブルの整形とピボットテーブル
- テキストデータの操作
- 欠損データの操作
- 重複ラベル
- カテゴリデータ
- Group by - 分割・適用・結合
カテゴリデータ
ここでは、Rのfactor
との簡単な比較を交えて、pandasのカテゴリデータ型を紹介します。
カテゴリ型Categoricals
は、統計学におけるカテゴリ変数に対応するpandasのデータ型です。カテゴリ変数は、限られた、通常は固定された数の可能な値(categories
、Rではlevels
)を取ります。例えば、性別・社会階級・血液型・所属する国・観測時間・リッカート尺度による評価などです。
統計的カテゴリ変数とは対照的に、カテゴリデータは順序を持つかもしれません(たとえば、「強くそう思う」対「そう思う」、「最初の観察」対「2番目の観察」)。ただし数値演算(加算・除算など)はできません。
カテゴリデータの値はすべてcategories
に含まれるかnp.nan
です。順序は、値の辞書的順序ではなく、categories
の順序によって定義されます。データ構造は内部的には、categories
配列と、categories
配列の実値を指し示す整数配列code
から構成されています。
カテゴリデータ型は、以下のような場合に有用です。
- 数種類の値のみからなる文字列変数。このような文字列変数をカテゴリ変数に変換すると、メモリが多少節約できます。こちらを参照してください。
- 変数の辞書的順序(訳注:アルファベット順や五十音順、より正確には文字コード順)が論理的順序("one"、"two"、"three")と異なる場合。カテゴリ変数に変換してカテゴリの順序を指定することで、ソートやmin/maxは辞書順ではなく論理順に基づくようになります。こちらを参照してください。
- 他のPythonライブラリに対して、(例えば、適切な統計手法やプロットタイプを使用するために)このカラムがカテゴリ変数として扱われるべきであるという標識になります。
カテゴリカルに関するAPIドキュメントも参照してください。
オブジェクトの作成
シリーズの作成
カテゴリカルなSeries
またはDataFrame
の列はいくつかの方法で作成することができます。
Series
を作成する際に、dtype="category"
を指定する方法。
s = pd.Series(["a", "b", "c", "a"], dtype="category")
s
Out[2]:
# 0 a
# 1 b
# 2 c
# 3 a
# dtype: category
# Categories (3, object): ['a', 'b', 'c']
既存のSeries
や列をcategory
データ型に変換する方法。
df = pd.DataFrame({"A": ["a", "b", "c", "a"]})
df["B"] = df["A"].astype("category")
df
Out[5]:
# A B
# 0 a a
# 1 b b
# 2 c c
# 3 a a
データを離散的なビンにグループ化するcut()
などの、特殊な関数を使用する方法。ドキュメントのタイリングの例を参照してください。
df = pd.DataFrame({"value": np.random.randint(0, 100, 20)})
labels = ["{0} - {1}".format(i, i + 9) for i in range(0, 100, 10)]
df["group"] = pd.cut(df.value, range(0, 105, 10), right=False, labels=labels)
df.head(10)
Out[9]:
# value group
# 0 65 60 - 69
# 1 49 40 - 49
# 2 56 50 - 59
# 3 43 40 - 49
# 4 43 40 - 49
# 5 91 90 - 99
# 6 32 30 - 39
# 7 87 80 - 89
# 8 36 30 - 39
# 9 8 0 - 9
pandas.Categorical
オブジェクトをSeries
に渡したり、DataFrame
に代入したりする方法。
raw_cat = pd.Categorical(
["a", "b", "c", "a"], categories=["b", "c", "d"], ordered=False
)
s = pd.Series(raw_cat)
s
# 0 NaN
# 1 b
# 2 c
# 3 NaN
# dtype: category
# Categories (3, object): ['b', 'c', 'd']
df = pd.DataFrame({"A": ["a", "b", "c", "a"]})
df["B"] = raw_cat
df
# A B
# 0 a NaN
# 1 b b
# 2 c c
# 3 a NaN
カテゴリデータは特定のcategory
データ型を持ちます。
df.dtypes
# A object
# B category
# dtype: object
データフレームの作成
1つの列をカテゴリデータに変換した前節と同様に、DataFrame
のすべての列は、構築中または構築後にカテゴリデータに一括変換することができます。
これは、DataFrame
のコンストラクタでdtype="category"
を指定することにより、構築中に行うことができます。
df = pd.DataFrame({"A": list("abca"), "B": list("bccd")}, dtype="category")
df.dtypes
# A category
# B category
# dtype: object
各列に存在するカテゴリは異なることに注意してください。変換は列ごとに行われるため、ある列に存在するラベルのみがカテゴリとなります。
df["A"]
# 0 a
# 1 b
# 2 c
# 3 a
# Name: A, dtype: category
# Categories (3, object): ['a', 'b', 'c']
df["B"]
# 0 b
# 1 c
# 2 c
# 3 d
# Name: B, dtype: category
# Categories (3, object): ['b', 'c', 'd']
同様に、既存のDataFrame
の全ての列をDataFrame.astype()
で一括変換できます。
df = pd.DataFrame({"A": list("abca"), "B": list("bccd")})
df_cat = df.astype("category")
df_cat.dtypes
# A category
# B category
# dtype: object
この変換も同様に列ごとに行われます。
df_cat["A"]
# 0 a
# 1 b
# 2 c
# 3 a
# Name: A, dtype: category
# Categories (3, object): ['a', 'b', 'c']
df_cat["B"]
# 0 b
# 1 c
# 2 c
# 3 d
# Name: B, dtype: category
# Categories (3, object): ['b', 'c', 'd']
挙動のコントロール
dtype='category'
を渡した上記の例では、デフォルトの挙動を使用しました。
- カテゴリはデータから推測される。
- カテゴリは順序付けされない。
これらの動作を制御するには、'category'
を渡す代わりに、CategoricalDtype
のインスタンスを使用します。
from pandas.api.types import CategoricalDtype
s = pd.Series(["a", "b", "c", "a"])
cat_type = CategoricalDtype(categories=["b", "c", "d"], ordered=True)
s_cat = s.astype(cat_type)
s_cat
# 0 NaN
# 1 b
# 2 c
# 3 NaN
# dtype: category
# Categories (3, object): ['b' < 'c' < 'd']
同様に、CategoricalDtype
をDataFrame
で使用することで、すべての列でカテゴリが一貫していることを保証することができます。
from pandas.api.types import CategoricalDtype
df = pd.DataFrame({"A": list("abca"), "B": list("bccd")})
cat_type = CategoricalDtype(categories=list("abcd"), ordered=True)
df_cat = df.astype(cat_type)
df_cat["A"]
# 0 a
# 1 b
# 2 c
# 3 a
# Name: A, dtype: category
# Categories (4, object): ['a' < 'b' < 'c' < 'd']
df_cat["B"]
# 0 b
# 1 c
# 2 c
# 3 d
# Name: B, dtype: category
# Categories (4, object): ['a' < 'b' < 'c' < 'd']
DataFrame
全体のラベルを各列のカテゴリとして使用するような、テーブル単位の変換を行うには、categories
パラメータをcategories = pd.unique(df.to_numpy().ravel())
としてプログラム的に決定します。
すでにcodes
とcategories
がある場合は、from_codes()
コンストラクタを使用することで、通常のコンストラクタ・モードにおけるファクタライズ(コード化)のステップを省くことができます。
splitter = np.random.choice([0, 1], 5, p=[0.5, 0.5])
s = pd.Series(pd.Categorical.from_codes(splitter, categories=["train", "test"]))
元のデータを復元する
元のSeries
やNumPy配列に戻すには、Series.astype(original_dtype)
やnp.asarray(categorical)
を使用します。
s = pd.Series(["a", "b", "c", "a"])
s
# 0 a
# 1 b
# 2 c
# 3 a
# dtype: object
s2 = s.astype("category")
s2
# 0 a
# 1 b
# 2 c
# 3 a
# dtype: category
# Categories (3, object): ['a', 'b', 'c']
s2.astype(str)
# 0 a
# 1 b
# 2 c
# 3 a
# dtype: object
np.asarray(s2)
# array(['a', 'b', 'c', 'a'], dtype=object)
Rのfactor
関数とは対照的に、カテゴリデータは入力値を文字列に変換せず、カテゴリは最終的に元の値と同じデータ型になります。
Rのfactor
関数とは対照的に、現在のところ、作成時にラベルを割り当てたり変更したりする方法はありません。作成後にカテゴリを変更する場合は、categories
を使用してください。
CategoricalDtype(カテゴリデータ型)
カテゴリ型は次のものから構成されています。
-
categories
:一意な値の並びで、欠損値がない -
ordered
:真偽値
この情報は、CategoricalDtype
に格納することができます。categories
引数はオプションで、これは実際のカテゴリはpandas.Categorical
が作成されたときにデータ内に存在するものから推測する必要があることを意味します。カテゴリはデフォルトで順序がないものと仮定されます。
from pandas.api.types import CategoricalDtype
CategoricalDtype(["a", "b", "c"])
# CategoricalDtype(categories=['a', 'b', 'c'], ordered=False)
CategoricalDtype(["a", "b", "c"], ordered=True)
# CategoricalDtype(categories=['a', 'b', 'c'], ordered=True)
CategoricalDtype()
# CategoricalDtype(categories=None, ordered=False)
CategoricalDtype
はpandasがdtype
を受け取るあらゆる場面で使用することができます。例えばpandas.read_csv()
・pandas.read_csv()
やSeries
コンストラクタの中で使用できます。
利便性のために、カテゴリを順序付けせず、かつ配列に存在する値のセットと同じにする、デフォルトの動作が必要な場合は、CategoricalDtype
の代わりに文字列'category'
を使用することができます。言い換えると、dtype='category'
はdtype=CategoricalDtype()
と同じ意味になります。
等価性セマンティクス
CategoricalDtype
の2つのインスタンスは、カテゴリと順序が同じであれば、常に等しいと比較されます。順序のない2つのカテゴリデータを比較する場合、categories
の順序は考慮されません。
c1 = CategoricalDtype(["a", "b", "c"], ordered=False)
# ordered=Falseのときは順序を考慮しないので、等しい
c1 == CategoricalDtype(["b", "c", "a"], ordered=False)
# True
# 2つ目のCategoricalDtypeは順序があるため、等しくない
c1 == CategoricalDtype(["a", "b", "c"], ordered=True)
# False
CategoricalDtype
のインスタンスはすべて、比較演算において文字列'category'
と等しくなります。
c1 == "category"
# True
dtype='category'
は簡潔にいえばCategoricalDtype(None, False)
であり、すべてのCategoricalDtype
のインスタンスは比較演算において'category'
と等しくなるので、カテゴリや順序に関係なく、すべてのCategoricalDtype
のインスタンスはCategoricalDtype(None, False)
と等しいと言えます。
describeメソッド
カテゴリデータに対してdescribe()
を使用すると、string
型のSeries
またはDataFrame
と同様の出力が得られます。
cat = pd.Categorical(["a", "c", "c", np.nan], categories=["b", "a", "c"])
df = pd.DataFrame({"cat": cat, "s": ["a", "c", "c", np.nan]})
df.describe()
# cat s
# count 3 3
# unique 2 2
# top c c
# freq 2 2
df["cat"].describe()
# count 3
# unique 2
# top c
# freq 2
# Name: cat, dtype: object
カテゴリの操作
カテゴリデータにはcategories
とordered
プロパティがあり、データの取り得る値や順序が重要かどうかが列挙されています。これらのプロパティはs.cat.categories
およびs.cat.ordered
として公開されています。カテゴリと順序を手動で指定しない場合、それらは渡された引数から推論されます。
s = pd.Series(["a", "b", "c", "a"], dtype="category")
s.cat.categories
# Index(['a', 'b', 'c'], dtype='object')
s.cat.ordered
# False
カテゴリを指定した順番で渡すことも可能です。
s = pd.Series(pd.Categorical(["a", "b", "c", "a"], categories=["c", "b", "a"]))
s.cat.categories
# Index(['c', 'b', 'a'], dtype='object')
s.cat.ordered
# False
新しいカテゴリデータは自動的に順序付けされません。順序付きCategorical
を指示するには、ordered=True
を明示的に渡す必要があります。
unique()
の結果はSeries.cat.categories
と同じとは限りません。なぜならSeries.unique()
には、カテゴリを出現順に返すことと、実際に存在する値のみを含むという、いくつかの保証があるからです。
s = pd.Series(list("babc")).astype(CategoricalDtype(list("abcd")))
s
# 0 b
# 1 a
# 2 b
# 3 c
# dtype: category
# Categories (4, object): ['a', 'b', 'c', 'd']
# categories
s.cat.categories
# Index(['a', 'b', 'c', 'd'], dtype='object')
# uniques
s.unique()
# ['b', 'a', 'c']
# Categories (4, object): ['a', 'b', 'c', 'd']
カテゴリのリネーム
カテゴリの名前を変更するには、rename_categories()
メソッドを使用します。
s = pd.Series(["a", "b", "c", "a"], dtype="category")
s
# 0 a
# 1 b
# 2 c
# 3 a
# dtype: category
# Categories (3, object): ['a', 'b', 'c']
s = s.cat.rename_categories([1, 2, 3])
s
# 0 1
# 1 2
# 2 3
# 3 1
# dtype: category
# Categories (3, int64): [1, 2, 3]
# 辞書ライクのオブジェクトを渡して、リネームのマッピングを行うことも可能
s = s.cat.rename_categories({1: "x", 2: "y", 3: "z"})
s
# 0 x
# 1 y
# 2 z
# 3 x
# dtype: category
# Categories (3, object): ['x', 'y', 'z']
Rのfactor
とは対照的に、カテゴリデータは文字列以外の型のカテゴリを持つことができます。
新しいカテゴリの割り当てはインプレース操作であり、Series.cat
の他のほとんどの操作はデフォルトでcategory
データ型の新しいSeries
を返すことに注意してください。
カテゴリは一意でなければならず、そうでない場合はValueError
が発生します。
try:
s.cat.rename_categories([1, 1, 1])
except ValueError as e:
print("ValueError:", str(e))
# ValueError: Categorical categories must be unique
また、カテゴリはNaNがあってはならず、存在する場合はValueError
が発生します。
try:
s = s.cat.rename_categories([1, 2, np.nan])
except ValueError as e:
print("ValueError:", str(e))
# ValueError: Categorical categories cannot be null
新しいカテゴリーの追加
カテゴリの追加は、add_categories()
メソッドで行うことができます。
s = s.cat.add_categories([4])
s.cat.categories
# Index(['x', 'y', 'z', 4], dtype='object')
s
# 0 x
# 1 y
# 2 z
# 3 x
# dtype: category
# Categories (4, object): ['x', 'y', 'z', 4]
未使用のカテゴリの削除
未使用のカテゴリ(データに存在しない値)を削除することも可能です。
s = pd.Series(pd.Categorical(["a", "b", "a"], categories=["a", "b", "c", "d"]))
s
# 0 a
# 1 b
# 2 a
# dtype: category
# Categories (4, object): ['a', 'b', 'c', 'd']
s.cat.remove_unused_categories()
# 0 a
# 1 b
# 2 a
# dtype: category
# Categories (2, object): ['a', 'b']
カテゴリの設定
カテゴリの削除と追加を一度に行いたい場合(これは速度的に有利です)、 あるいは単にカテゴリをあらかじめ定義された尺度に設定したい場合は、set_categories()
を使用します。
s = pd.Series(["one", "two", "four", "-"], dtype="category")
s
# 0 one
# 1 two
# 2 four
# 3 -
# dtype: category
# Categories (4, object): ['-', 'four', 'one', 'two']
s = s.cat.set_categories(["one", "two", "three", "four"])
s
# 0 one
# 1 two
# 2 four
# 3 NaN
# dtype: category
# Categories (4, object): ['one', 'two', 'three', 'four']
Categorical.set_categories()
は、あるカテゴリが意図的に省略されたのか、スペルミスのためか、(Python3では)型の違い(例えば、NumPy S1 dtypeとPython string)のためか分からないので注意してください。これは、驚くべき振る舞いをもたらすかもしれません!
ソートと順序
カテゴリデータが順序付けされている場合(s.cat.ordered == True
)は、カテゴリの順序に意味があり、特定の操作が可能になります。カテゴリデータが順序付けされていない場合は、.min()
・.max()
はTypeError
を発生させます。
s = pd.Series(pd.Categorical(["a", "b", "c", "a"], ordered=False))
s.sort_values(inplace=True)
s = pd.Series(["a", "b", "c", "a"]).astype(CategoricalDtype(ordered=True))
s.sort_values(inplace=True)
s
# 0 a
# 3 a
# 1 b
# 2 c
# dtype: category
# Categories (3, object): ['a' < 'b' < 'c']
s.min(), s.max()
# ('a', 'c')
カテゴリデータを順序付きにするにはas_ordered()
を、順序なしにするにはas_unordered()
を使用できます。これらは、デフォルトで新しいオブジェクトを返します。
s.cat.as_ordered()
# 0 a
# 3 a
# 1 b
# 2 c
# dtype: category
# Categories (3, object): ['a' < 'b' < 'c']
s.cat.as_unordered()
# 0 a
# 3 a
# 1 b
# 2 c
# dtype: category
# Categories (3, object): ['a', 'b', 'c']
ソートは、データ型に基づく辞書的な順序ではなく、カテゴリで定義された順序を使用します。これは文字列データにも数値データも等しく動作します。
s = pd.Series([1, 2, 3, 1], dtype="category")
s = s.cat.set_categories([2, 3, 1], ordered=True)
s
# 0 1
# 1 2
# 2 3
# 3 1
# dtype: category
# Categories (3, int64): [2 < 3 < 1]
s.sort_values(inplace=True)
s
# 1 2
# 2 3
# 0 1
# 3 1
# dtype: category
# Categories (3, int64): [2 < 3 < 1]
s.min(), s.max()
# (2, 1)
順序の変更
カテゴリの順序の変更はCategorical.reorder_categories()
とCategorical.set_categories()
メソッドで行うことができます。Categorical.reorder_categories()
では、すべての古いカテゴリが新しいカテゴリに含まれていなければならず、またカテゴリの追加は許可されません。これは、必然的にソート順がカテゴリー順と同じになります。
s = pd.Series([1, 2, 3, 1], dtype="category")
s = s.cat.reorder_categories([2, 3, 1], ordered=True)
s
# 0 1
# 1 2
# 2 3
# 3 1
# dtype: category
# Categories (3, int64): [2 < 3 < 1]
s.sort_values(inplace=True)
s
# 1 2
# 2 3
# 0 1
# 3 1
# dtype: category
# Categories (3, int64): [2 < 3 < 1]
s.min(), s.max()
# (2, 1)
新しいカテゴリの割り当てとカテゴリの順序変更の違いに注意してください。前者はカテゴリの名前を変更するため、Series
内の個々の値を変更しますが、最初の位置が最後にソートされていた場合、名前を変更した値は依然として最後にソートされます。順序変更は、値の順序変更の方法が後で変わることを意味しますが、シリーズの個々の値が変更されることはありません。
Categorical
が順序付けされていない場合、Series.min()
とSeries.max()
はTypeError
が発生します。また、+
・-
・*
・/
などの数値演算やそれらに基づく演算(例えばSeries.median()
は配列の長さが偶数の場合、2つの値の間の平均を計算する必要があります)は動作せず、TypeError
が発生します。
複数列のソート
カテゴリデータ型の列は、他の列と同様に複数列のソートに参加させることができます。カテゴリデータの順序はその列のcategories
によって決定されます。
dfs = pd.DataFrame(
{
"A": pd.Categorical(
list("bbeebbaa"),
categories=["e", "a", "b"],
ordered=True,
),
"B": [1, 2, 1, 2, 2, 1, 2, 1],
}
)
dfs.sort_values(by=["A", "B"])
# A B
# 2 e 1
# 3 e 2
# 7 a 1
# 6 a 2
# 0 b 1
# 5 b 1
# 1 b 2
# 4 b 2
categories
の順序変更は、将来のソートを変更します。
dfs["A"] = dfs["A"].cat.reorder_categories(["a", "b", "e"])
dfs.sort_values(by=["A", "B"])
# A B
# 7 a 1
# 6 a 2
# 0 b 1
# 5 b 1
# 1 b 2
# 4 b 2
# 2 e 1
# 3 e 2
比較演算
カテゴリデータと他のオブジェクトとの比較は、次の3つの場合可能です。
- カテゴリデータと同じ長さのリストライクオブジェクト(
list
・Series
・array
等)との等価性の比較(==
と!=
) -
ordered==True
でcategories
が同じであるカテゴリデータと別のカテゴリSeries
との全ての比較(==
・!=
・>
・>=
・<
・<=
). - カテゴリデータとスカラーとの全ての比較。
その他の比較、特にカテゴリが異なる2つのカテゴリデータや、カテゴリデータとリストライクオブジェクトにおける「非等価性」比較(>
・>=
・<
・<=
)は、すべてTypeError
を発生させます。
カテゴリデータとSeries
・np.array
・list
、またはカテゴリや順序が異なるカテゴリデータの「非等価性」比較は、すべてTypeError
が発生します。これは、カスタムの順序付きカテゴリの比較は、順序を考慮したものと考慮しないものの2通りの解釈が可能なためです。
cat = pd.Series([1, 2, 3]).astype(CategoricalDtype([3, 2, 1], ordered=True))
cat_base = pd.Series([2, 2, 2]).astype(CategoricalDtype([3, 2, 1], ordered=True))
cat_base2 = pd.Series([2, 2, 2]).astype(CategoricalDtype(ordered=True))
cat
# 0 1
# 1 2
# 2 3
# dtype: category
# Categories (3, int64): [3 < 2 < 1]
cat_base
# 0 2
# 1 2
# 2 2
# dtype: category
# Categories (3, int64): [3 < 2 < 1]
cat_base2
# 0 2
# 1 2
# 2 2
# dtype: category
# Categories (1, int64): [2]
同じカテゴリと順序を持つカテゴリデータとの比較や、スカラーとの比較は有効です。
cat > cat_base
# 0 True
# 1 False
# 2 False
# dtype: bool
cat > 2
# 0 True
# 1 False
# 2 False
# dtype: bool
等価性の比較は、同じ長さのリストライクオブジェクトとスカラーで動作します。
cat == cat_base
# 0 False
# 1 True
# 2 False
# dtype: bool
cat == np.array([1, 2, 3])
# 0 True
# 1 True
# 2 True
# dtype: bool
cat == 2
# 0 False
# 1 True
# 2 False
# dtype: bool
次の例は、カテゴリが異なるのでうまくいきません。
try:
cat > cat_base2
except TypeError as e:
print("TypeError:", str(e))
# TypeError: Categoricals can only be compared if 'categories' are the same.
もし、カテゴリデータではないリストライクオブジェクトとカテゴリシリーズとの「非等価性」比較を行いたい場合は、明示的にカテゴリデータを元の値に変換する必要があります。
base = np.array([1, 2, 3])
try:
cat > base
except TypeError as e:
print("TypeError:", str(e))
# TypeError: Cannot compare a Categorical for op __gt__ with type <class 'numpy.ndarray'>.
# If you want to compare values, use 'np.asarray(cat) <op> other'.
np.asarray(cat) > base
# array([False, False, False])
同じカテゴリを持つ2つの順序なしカテゴリカルを比較する場合、順序は考慮されません。
c1 = pd.Categorical(["a", "b"], categories=["a", "b"], ordered=False)
c2 = pd.Categorical(["a", "b"], categories=["b", "a"], ordered=False)
c1 == c2
# array([ True, True])
操作
Series.min()
・Series.max()
・Series.mode()
以外に、カテゴリデータに対しては以下の操作が可能です。
Series.value_counts()
などのSeries
メソッドは、データに存在しないカテゴリがあっても、すべてのカテゴリを使用します。
s = pd.Series(pd.Categorical(["a", "b", "c", "c"], categories=["c", "a", "b", "d"]))
s.value_counts()
# c 2
# a 1
# b 1
# d 0
# dtype: int64
DataFrame.sum()
のようなDataFrame
のメソッドも、「未使用」のカテゴリーを表示します。
columns = pd.Categorical(
["One", "One", "Two"], categories=["One", "Two", "Three"], ordered=True
)
df = pd.DataFrame(
data=[[1, 2, 3], [4, 5, 6]],
columns=pd.MultiIndex.from_arrays([["A", "B", "B"], columns]),
)
df.groupby(axis=1, level=1).sum()
# One Two Three
# 0 3 3 0
# 1 9 6 0
Groupbyも「未使用」のカテゴリーが表示されます。
cats = pd.Categorical(
["a", "b", "b", "b", "c", "c", "c"], categories=["a", "b", "c", "d"]
)
df = pd.DataFrame({"cats": cats, "values": [1, 2, 2, 2, 3, 4, 5]})
df.groupby("cats").mean()
# values
# cats
# a 1.0
# b 2.0
# c 4.0
# d NaN
cats2 = pd.Categorical(["a", "a", "b", "b"], categories=["a", "b", "c"])
df2 = pd.DataFrame(
{
"cats": cats2,
"B": ["c", "d", "c", "d"],
"values": [1, 2, 3, 4],
}
)
df2.groupby(["cats", "B"]).mean()
# values
# cats B
# a c 1.0
# d 2.0
# b c 3.0
# d 4.0
# c c NaN
# d NaN
ピボットテーブルの場合。
raw_cat = pd.Categorical(["a", "a", "b", "b"], categories=["a", "b", "c"])
df = pd.DataFrame({"A": raw_cat, "B": ["c", "d", "c", "d"], "values": [1, 2, 3, 4]})
pd.pivot_table(df, values="values", index=["A", "B"])
# values
# A B
# a c 1
# d 2
# b c 3
# d 4
データの加工
最適化されたpandasのデータアクセスメソッドである.loc
・.iloc
・.at
・.iat
は通常通り動作します。唯一の違いは、(取得の際の)戻り値の型と、すでにcategories
にある値のみを代入できることです。
取得
スライス操作がDataFrame
またはSeries
型の列を返す場合、category
データ型は保持されます。
idx = pd.Index(["h", "i", "j", "k", "l", "m", "n"])
cats = pd.Series(["a", "b", "b", "b", "c", "c", "c"], dtype="category", index=idx)
values = [1, 2, 2, 2, 3, 4, 5]
df = pd.DataFrame({"cats": cats, "values": values}, index=idx)
df.iloc[2:4, :]
# cats values
# j b 2
# k b 2
df.iloc[2:4, :].dtypes
# cats category
# values int64
# dtype: object
df.loc["h":"j", "cats"]
# h a
# i b
# j b
# Name: cats, dtype: category
# Categories (3, object): ['a', 'b', 'c']
df[df["cats"] == "b"]
# cats values
# i b 2
# j b 2
# k b 2
カテゴリ型が保存されない例として、1つの行を取り出した場合、結果のSeries
はobject
データ型になります。
# "h"の行を全てSeriesとして取得
df.loc["h", :]
# cats a
# values 1
# Name: h, dtype: object
カテゴリデータから単一要素を返す場合も、長さ"1"のカテゴリデータではなく、値を返します。
df.iat[0, 0]
# 'a'
df["cats"] = df["cats"].cat.rename_categories(["x", "y", "z"])
df.at["h", "cats"] # 文字列を返す
# 'x'
これはRのfactor
関数とは対照的で、factor(c(1,2,3))[1]
は1つの値のfactor
を返します。
カテゴリ型の単一値Series
を取得するには、単一値を持つリストを渡します。
df.loc[["h"], "cats"]
# h x
# Name: cats, dtype: category
# Categories (3, object): ['x', 'y', 'z']
文字列と日付アクセサ
.dt
と.str
アクセサは、s.cat.category
が適切な型であれば機能します。
str_s = pd.Series(list("aabb"))
str_cat = str_s.astype("category")
str_cat
# 0 a
# 1 a
# 2 b
# 3 b
# dtype: category
# Categories (2, object): ['a', 'b']
str_cat.str.contains("a")
# 0 True
# 1 True
# 2 False
# 3 False
# dtype: bool
date_s = pd.Series(pd.date_range("1/1/2015", periods=5))
date_cat = date_s.astype("category")
date_cat
# 0 2015-01-01
# 1 2015-01-02
# 2 2015-01-03
# 3 2015-01-04
# 4 2015-01-05
# dtype: category
# Categories (5, datetime64[ns]): [2015-01-01, 2015-01-02, 2015-01-03, 2015-01-04, 2015-01-05]
date_cat.dt.day
# 0 1
# 1 2
# 2 3
# 3 4
# 4 5
# dtype: int64
返されるSeries
(またはDataFrame
)は、その型のシリーズに.str.<method>
/.dt.<method>
を使用した場合と同じ型になります(category
型にはなりません!)。
つまり、Series
のアクセサのメソッドやプロパティから返される値と、そのSeries
をcategory
型に変換した値のアクセサのメソッドやプロパティから返される値とは、等しい値になります。
ret_s = str_s.str.contains("a")
ret_cat = str_cat.str.contains("a")
ret_s.dtype == ret_cat.dtype
# True
ret_s == ret_cat
# 0 True
# 1 True
# 2 True
# 3 True
# dtype: bool
この処理はcategories
に対して行われ、その後、新しいSeries
が構築されます。これは、多くの要素が繰り返される文字列型のSeries
がある場合(つまり、Series
内のユニークな要素の数がSeries
の長さよりもずっと小さい場合)、パフォーマンスに影響を与えます。その場合、元のSeries
をcategory
型に変換し、その上で.str.<method>
または.dt.<property>
を使用すると速くなることがあります。
代入操作
カテゴリ列(またはSeries
)に値を設定する操作は、値がカテゴリに含まれている限り機能します。
idx = pd.Index(["h", "i", "j", "k", "l", "m", "n"])
cats = pd.Categorical(["a", "a", "a", "a", "a", "a", "a"], categories=["a", "b"])
values = [1, 1, 1, 1, 1, 1, 1]
df = pd.DataFrame({"cats": cats, "values": values}, index=idx)
df.iloc[2:4, :] = [["b", 2], ["b", 2]]
df
# cats values
# h a 1
# i a 1
# j b 2
# k b 2
# l a 1
# m a 1
# n a 1
try:
df.iloc[2:4, :] = [["c", 3], ["c", 3]]
except TypeError as e:
print("TypeError:", str(e))
# TypeError: Cannot setitem on a Categorical with a new category, set the categories first
カテゴリデータを割り当てて値を設定すると、categories
が一致するかどうかもチェックされます。
df.loc["j":"k", "cats"] = pd.Categorical(["a", "a"], categories=["a", "b"])
df
# cats values
# h a 1
# i a 1
# j a 2
# k a 2
# l a 1
# m a 1
# n a 1
try:
df.loc["j":"k", "cats"] = pd.Categorical(["b", "b"], categories=["a", "b", "c"])
except TypeError as e:
print("TypeError:", str(e))
# TypeError: Cannot set a Categorical with another, without identical categories
Categorical
を他の型のカラムの一部に割り当てると、値が使用されます。
df = pd.DataFrame({"a": [1, 1, 1, 1, 1], "b": ["a", "a", "a", "a", "a"]})
df.loc[1:2, "a"] = pd.Categorical(["b", "b"], categories=["a", "b"])
df.loc[2:3, "b"] = pd.Categorical(["b", "b"], categories=["a", "b"])
df
# a b
# 0 1 a
# 1 b a
# 2 b b
# 3 1 b
# 4 1 a
df.dtypes
# a object
# b object
# dtype: object
結合・連結
デフォルトでは、同じカテゴリを含むSeries
またはDataFrame
を結合するとcategory
データ型になり、そうでない場合は基になるカテゴリのデータ型に依存します。結果が非カテゴリデータ型になるような結合操作は、おそらくメモリ使用量が高くなります。.astype
またはunion_categoricals
を使用して、category
の結果を確実にしてください。
from pandas.api.types import union_categoricals
# カテゴリが等しい場合
s1 = pd.Series(["a", "b"], dtype="category")
s2 = pd.Series(["a", "b", "a"], dtype="category")
pd.concat([s1, s2])
Out[188]:
# 0 a
# 1 b
# 0 a
# 1 b
# 2 a
# dtype: category
# Categories (2, object): ['a', 'b']
# カテゴリが異なる場合
s3 = pd.Series(["b", "c"], dtype="category")
pd.concat([s1, s3])
# 0 a
# 1 b
# 0 b
# 1 c
# dtype: object
# 出力データ型はカテゴリの値から推定される
int_cats = pd.Series([1, 2], dtype="category")
float_cats = pd.Series([3.0, 4.0], dtype="category")
pd.concat([int_cats, float_cats])
# 0 1.0
# 1 2.0
# 0 3.0
# 1 4.0
# dtype: float64
pd.concat([s1, s3]).astype("category")
# 0 a
# 1 b
# 0 b
# 1 c
# dtype: category
# Categories (3, object): ['a', 'b', 'c']
union_categoricals([s1.array, s3.array])
# ['a', 'b', 'b', 'c']
# Categories (3, object): ['a', 'b', 'c']
Categorical
を結合した結果は以下のようになります。
引数1 | 引数2 | カテゴリ一致 | 結果 |
---|---|---|---|
カテゴリ | カテゴリ | True | カテゴリ |
カテゴリ(オブジェクト) | カテゴリ(オブジェクト) | False | オブジェクト(データ型は推定される) |
カテゴリ(整数) | カテゴリ(浮動小数) | False | 浮動小数(データ型は推定される) |
結合時のデータ型の保存とパフォーマンスに関する注意事項については、mergeのデータ型のセクションも参照してください。
カテゴリの結合(union)
必ずしも同じカテゴリを持たないカテゴリを結合したい場合、union_categoricals()
関数はリストライクなカテゴリデータを結合します。新しいカテゴリは、結合されたカテゴリの和(union)となります。
from pandas.api.types import union_categoricals
a = pd.Categorical(["b", "c"])
b = pd.Categorical(["a", "b"])
union_categoricals([a, b])
# ['b', 'c', 'a', 'b']
# Categories (3, object): ['b', 'c', 'a']
デフォルトでは、結果のカテゴリはデータに出現する順番に並べられます。もしカテゴリをレックスソートさせたい場合は、sort_categories=True
引数を使用します。
union_categoricals([a, b], sort_categories=True)
# ['b', 'c', 'a', 'b']
# Categories (3, object): ['a', 'b', 'c']
union_categoricals
は、同じカテゴリと順序情報を持つ2つのカテゴリを組み合わせるという「簡単な」ケースでも動作します(例えば、append
でも可能な操作)。
a = pd.Categorical(["a", "b"], ordered=True)
b = pd.Categorical(["a", "b", "a"], ordered=True)
union_categoricals([a, b])
# ['a', 'b', 'a', 'b', 'a']
# Categories (2, object): ['a' < 'b']
次の例は、カテゴリが順番付けされており、同一ではないため、TypeError
が発生します。
a = pd.Categorical(["a", "b"], ordered=True)
b = pd.Categorical(["a", "b", "c"], ordered=True)
union_categoricals([a, b])
# TypeError: to union ordered Categoricals, all categories must be the same
カテゴリや順序が異なる順序付きカテゴリデータは、ignore_ordered=True
引数を用いて結合することができます。
a = pd.Categorical(["a", "b", "c"], ordered=True)
b = pd.Categorical(["c", "b", "a"], ordered=True)
union_categoricals([a, b], ignore_order=True)
# ['a', 'b', 'c', 'c', 'b', 'a']
# Categories (3, object): ['a', 'b', 'c']
union_categoricals()
はCategoricalIndex
やカテゴリデータを格納するSeries
でも動作しますが、結果の配列は常にプレーンなCategorical
であることに注意してください。
union_categoricals
はカテゴリを結合する際に、カテゴリの整数コードを再コード化することがあります。これはおそらくあなたが望むことですが、もしあなたがカテゴリの正確な番号付けに依存している場合は、注意してください。
c1 = pd.Categorical(["b", "c"])
c2 = pd.Categorical(["a", "b"])
c1
# ['b', 'c']
# Categories (2, object): ['b', 'c']
# "b"は0にコード化されている
c1.codes
# array([0, 1], dtype=int8)
c2
# ['a', 'b']
# Categories (2, object): ['a', 'b']
# "b"は1にコード化されている
c2.codes
# array([0, 1], dtype=int8)
c = union_categoricals([c1, c2])
c
# ['b', 'c', 'a', 'b']
# Categories (3, object): ['b', 'c', 'a']
# "b"は1にコード化され、c1と等しいが、c2とは異なる
c.codes
# array([0, 1, 2, 0], dtype=int8)
データの読み込み/書き出し
category
データ型を含むデータをHDFStore
に書き込むことができます。例と注意点はこちらをご覧ください。
また、Stata形式のファイルにデータを書き込んだり、そこからデータを読み込んだりすることも可能です。例と注意点はこちらをご覧ください。
CSVファイルに書き出すと、データが変換され、カテゴリーに関する情報(カテゴリーや順序)が効果的に削除されます。したがって、CSVファイルを読み返す場合は、関連する列をcategory
に変換し直し、正しいカテゴリとカテゴリ順序を割り当てる必要があります。
import io
s = pd.Series(pd.Categorical(["a", "b", "b", "a", "a", "d"]))
# カテゴリのリネーム
s = s.cat.rename_categories(["very good", "good", "bad"])
# カテゴリ順序の変更と足りないカテゴリの追加
s = s.cat.set_categories(["very bad", "bad", "medium", "good", "very good"])
df = pd.DataFrame({"cats": s, "vals": [1, 2, 3, 4, 5, 6]})
csv = io.StringIO()
df.to_csv(csv)
df2 = pd.read_csv(io.StringIO(csv.getvalue()))
df2.dtypes
# Unnamed: 0 int64
# cats object
# vals int64
# dtype: object
df2["cats"]
# 0 very good
# 1 good
# 2 good
# 3 very good
# 4 very good
# 5 bad
# Name: cats, dtype: object
# カテゴリの復元
df2["cats"] = df2["cats"].astype("category")
df2["cats"].cat.set_categories(
["very bad", "bad", "medium", "good", "very good"], inplace=True
)
df2.dtypes
# Unnamed: 0 int64
# cats category
# vals int64
# dtype: object
df2["cats"]
# 0 very good
# 1 good
# 2 good
# 3 very good
# 4 very good
# 5 bad
# Name: cats, dtype: category
# Categories (5, object): ['very bad', 'bad', 'medium', 'good', 'very good']
to_sql
でSQL
データベースに書き込む場合も同様です。
欠損データ
pandasは主にnp.nan
という値を使って欠損データを表します。デフォルトでは計算には含まれません。欠損データのセクションを参照してください。
欠損値はカテゴリデータのcategories
には含まれず、values
のみに含まれます。その代わり、NaNは通常とは異なるものであり、常に存在する可能性があると認識されています。カテゴリデータのcodes
を扱う場合、欠損値は常に-1
のコードを持ちます。
s = pd.Series(["a", "b", np.nan, "a"], dtype="category")
# カテゴリは2つだけ
s
# 0 a
# 1 b
# 2 NaN
# 3 a
# dtype: category
# Categories (2, object): ['a', 'b']
s.cat.codes
# 0 0
# 1 1
# 2 -1
# 3 0
# dtype: int8
isna()
・fillna()
・dropna()
などの欠損データを扱うメソッドは、すべて正常に動作します。
s = pd.Series(["a", "b", np.nan], dtype="category")
s
# 0 a
# 1 b
# 2 NaN
# dtype: category
# Categories (2, object): ['a', 'b']
pd.isna(s)
# 0 False
# 1 False
# 2 True
# dtype: bool
s.fillna("a")
# 0 a
# 1 b
# 2 a
# dtype: category
# Categories (2, object): ['a', 'b']
Rのfactor
との相違点
Rのfactor関数とは、以下のような違いがあります。
- Rでいう
levels
は、categories
と名付けられています。 - Rの
levels
は常に文字列型ですが、pandasのcategories
は任意のデータ型を持つことができます。 - 作成時に特定のラベルを指定することはできません。作成後に
s.cat.rename_categories(new_labels)
を使ってください。 - Rの
factor
関数とは対照的に、新しいカテゴリシリーズを作成するための唯一の入力としてカテゴリデータを使用すると、使用されていないカテゴリは削除されず、渡されたカテゴリと等しい新しいカテゴリシリーズが作成されます! - Rは
levels
(pandasのcategories
)に欠損値を含めることができます。pandasはカテゴリとしてNaN
を持てませんが、欠損値をvalues
に含めることはできます。
その他
メモリ使用量
Categorical
のメモリ使用量は、カテゴリの数とデータの長さに比例します。一方、object
データ型は定数にデータの長さを掛けたものになります。
s = pd.Series(["foo", "bar"] * 1000)
# オブジェクトデータ型
s.nbytes
# 16000
# カテゴリデータ型
s.astype("category").nbytes
# 2016
カテゴリの数がデータの長さに近づくと、Categorical
は同等のobject
データ型による表現とほぼ同じかそれ以上のメモリを使用することになります。
s = pd.Series(["foo%04d" % i for i in range(2000)])
# オブジェクトデータ型
s.nbytes
# 16000
# カテゴリデータ型
s.astype("category").nbytes
# 20000
Categorical
はnumpy
配列ではない
現在、カテゴリデータとその基礎となるCategorical
はPythonのオブジェクトとして実装されており、低級のNumPy配列のデータ型ではありません。これはいくつかの問題を引き起こします。
NumPy自身は、新しいdtype
を認識しません。
try:
np.dtype("category")
except TypeError as e:
print("TypeError:", str(e))
# TypeError: data type 'category' not understood
dtype = pd.Categorical(["a"]).dtype
try:
np.dtype(dtype)
except TypeError as e:
print("TypeError:", str(e))
# TypeError: Cannot interpret 'CategoricalDtype(categories=['a'], ordered=False)' as a data type
データ型の比較は可能です。
dtype == np.str_
# False
np.str_ == dtype
# False
シリーズにカテゴリデータが含まれているかどうかを確認するには、hasattr(s, 'cat')
を使用します。
hasattr(pd.Series(["a"], dtype="category"), "cat")
# True
hasattr(pd.Series(["a"]), "cat")
# False
Categorical
は数値データではないので、(.categories
が数値であっても)category
型のSeries
にNumPy関数を使用してもうまくいかないはずです。
s = pd.Series(pd.Categorical([1, 2, 3, 4]))
try:
np.sum(s)
except TypeError as e:
print("TypeError:", str(e))
# TypeError: 'Categorical' with dtype category does not support reduction 'sum'
このような機能が動作する場合は、[https://github.com/pandas-dev/pandas]でバグを報告してください!
applyのデータ型
pandasは現在、apply関数においてデータ型を保持しません。行に沿って適用すると、object dtype
のSeries
が返却されます(行を取得して1つの要素を取得しようとすると基本的な型が返されるのと同じことです)。NaN
値は影響を受けません。関数を適用する前にfillna
を使用して欠損値を処理することができます。
df = pd.DataFrame(
{
"a": [1, 2, 3, 4],
"b": ["a", "b", "c", "d"],
"cats": pd.Categorical([1, 2, 3, 2]),
}
)
df.apply(lambda row: type(row["cats"]), axis=1)
# 0 <class 'int'>
# 1 <class 'int'>
# 2 <class 'int'>
# 3 <class 'int'>
# dtype: object
df.apply(lambda col: col.dtype, axis=0)
# a int64
# b object
# cats category
# dtype: object
カテゴリインデックス(CategoricalIndex)
CategoricalIndex
はインデックスの型の一種で、重複を伴うインデックスのサポートに有用です。これはCategorical
を囲むコンテナであり、多数の重複要素を持つインデックスの効率的なインデックス作成と保存を可能にします。より詳細な説明については、高度な索引に関するドキュメントを参照してください。
インデックスを設定すると、CategoricalIndex
が作成されます。
cats = pd.Categorical([1, 2, 3, 4], categories=[4, 2, 3, 1])
strings = ["a", "b", "c", "d"]
values = [4, 2, 3, 1]
df = pd.DataFrame({"strings": strings, "values": values}, index=cats)
df.index
# CategoricalIndex([1, 2, 3, 4], categories=[4, 2, 3, 1], ordered=False, dtype='category')
# カテゴリ順にソート
df.sort_index()
# strings values
# 4 d 1
# 2 b 2
# 3 c 3
# 1 a 4
副作用
Categorical
からSeries
を構築しても、入力したCategorical
はコピーされません。つまり、Series
を変更すると、ほとんどの場合、元のCategorical
が変更されます。
cat = pd.Categorical([1, 2, 3, 10], categories=[1, 2, 3, 4, 10])
s = pd.Series(cat, name="cat")
cat
# [1, 2, 3, 10]
# Categories (5, int64): [1, 2, 3, 4, 10]
s.iloc[0:2] = 10
cat
# [10, 10, 3, 10]
# Categories (5, int64): [1, 2, 3, 4, 10]
df = pd.DataFrame(s)
df["cat"].cat.categories = [1, 2, 3, 4, 5]
cat
# [5, 5, 3, 5]
# Categories (5, int64): [1, 2, 3, 4, 5]
copy=True
を使用してこのような動作を防ぐか、単にCategorical
を再利用しないようにします。
cat = pd.Categorical([1, 2, 3, 10], categories=[1, 2, 3, 4, 10])
s = pd.Series(cat, name="cat", copy=True)
cat
# [1, 2, 3, 10]
# Categories (5, int64): [1, 2, 3, 4, 10]
s.iloc[0:2] = 10
cat
# [1, 2, 3, 10]
# Categories (5, int64): [1, 2, 3, 4, 10]
これは、Categorical
ではなくNumPy配列を指定した場合にも起こります。整数配列(例えばnp.array([1,2,3,4])
)を使用すると同じ動作をしますが、文字列配列(例えばnp.array(["a", "b", "c", "a"])
)では同じ動作は起こりません。