LoginSignup
4
3

More than 1 year has passed since last update.

Pandasユーザーガイド「カテゴリデータ」(公式ドキュメント日本語訳)

Last updated at Posted at 2022-02-21

本記事は、Pandas の公式ドキュメントのUser Guide - Categorical dataを機械翻訳した後、一部の不自然な文章を手直ししたものである。

誤訳の指摘・代訳案・質問等があればコメント欄や編集リクエストでお願いします。

Pandas公式ドキュメント日本語訳記事一覧

カテゴリデータ

ここでは、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'を渡した上記の例では、デフォルトの挙動を使用しました。

  1. カテゴリはデータから推測される。
  2. カテゴリは順序付けされない。

これらの動作を制御するには、'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']

同様に、CategoricalDtypeDataFrameで使用することで、すべての列でカテゴリが一貫していることを保証することができます。

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())としてプログラム的に決定します。

すでにcodescategoriesがある場合は、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

カテゴリの操作

カテゴリデータにはcategoriesorderedプロパティがあり、データの取り得る値や順序が重要かどうかが列挙されています。これらのプロパティは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つの場合可能です。

  • カテゴリデータと同じ長さのリストライクオブジェクト(listSeriesarray等)との等価性の比較(==!=
  • ordered==Truecategoriesが同じであるカテゴリデータと別のカテゴリSeriesとの全ての比較(==!=>>=<<=).
  • カテゴリデータとスカラーとの全ての比較。

その他の比較、特にカテゴリが異なる2つのカテゴリデータや、カテゴリデータとリストライクオブジェクトにおける「非等価性」比較(>>=<<=)は、すべてTypeErrorを発生させます。

カテゴリデータとSeriesnp.arraylist、またはカテゴリや順序が異なるカテゴリデータの「非等価性」比較は、すべて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つの行を取り出した場合、結果のSeriesobjectデータ型になります。

# "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のアクセサのメソッドやプロパティから返される値と、そのSeriescategory型に変換した値のアクセサのメソッドやプロパティから返される値とは、等しい値になります。

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の長さよりもずっと小さい場合)、パフォーマンスに影響を与えます。その場合、元のSeriescategory型に変換し、その上で.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_sqlSQLデータベースに書き込む場合も同様です。

欠損データ

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

Categoricalnumpy配列ではない

現在、カテゴリデータとその基礎となる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 dtypeSeriesが返却されます(行を取得して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"]))では同じ動作は起こりません。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3