pandasで ValueError: cannot reindex from a duplicate axisのエラー

ValueError: cannot reindex from a duplicate axis

というエラーが、pandasにある。 エラーについて色々調べた結果、分からなかったこともあるけど、だいたい以下のとおりだろうか。

  • 行もしくは列の名前に重複がある場合に、特定の操作で発生するようだ?
  • 「複数の行/列のうちどれを使えばよいか分かりませんでした」というような意味かな?
  • pandasのバグを踏んでいる可能性もあるようだ?

最初の質問はエラー再現コードがない……

このエラーに関係するStack Overflowの質問のうち、一番閲覧数が多いのはこれだ。

https://stackoverflow.com/questions/27236275/what-does-valueerror-cannot-reindex-from-a-duplicate-axis-mean

……しかし、この質問にはエラーを再現させるコードが付属していない。 質問を投稿した人がエラーを出す短いコードを作ろうとしたが、うまく作れなかった、と書いてある。
俺自身も、質問者が書いたコードをちょっと変えてエラーを発生させてみようとしたけど、どうもうまく行かなかった。うーん。

(最初の質問が書かれたのが2014年なので、そこからpandasの仕様が変わってエラーが発生しなくなった、という可能性はある。)

ただ、解答者が名推理をして、「たぶん列名に重複があるんじゃないかな?」と書いたらそれが正解だったらしく、疑問は解決している。

reindexでエラー発生

pandasの公式ドキュメントの中で、ValueError: cannot reindex from a duplicate axisの記述がある部分は1箇所だけある。それはreindex関数の説明の中だ。

https://pandas.pydata.org/docs/user_guide/indexing.html?highlight=valueerror#reindexing

以下は、上記の公式ドキュメントと同じ内容である。

普通の使い方

まず、reindex()関数の普通の使い方を見てみよう。

import pandas as pd
import numpy as np
pd.options.display.notebook_repr_html = False  # jupyter notebook上での出力形式を制御するために書いています。無くても動きます。
# 動作環境の確認
print(pd.__version__)
print(np.__version__)

# --------------------

1.0.1
1.18.1
s = pd.Series([1, 2, 3])
s

# --------------------

0    1
1    2
2    3
dtype: int64

indexを指定して、元のオブジェクトの一部を選択・抽出できる。 もともとのSeriesに無いindex(ここでは3)を指定しても実行できるのが特徴である(該当する行にはNaNが入る)。

s.reindex([1, 2, 3])

# --------------------

1    2.0
2    3.0
3    NaN
dtype: float64

indexに重複があるとエラーが発生する

ここで、元のオブジェクトのindexに重複したものがあると、エラーValueError: cannot reindex from a duplicate axisが発生する。以下の通りだ。

s = pd.Series(np.arange(4), index=['a', 'a', 'b', 'c'])
s

# --------------------

a    0
a    1
b    2
c    3
dtype: int64
labels = ['c', 'd']
s.reindex(labels)

# --------------------

---------------------------------------------------------------------------
    ValueError                                Traceback (most recent call last)
    <ipython-input-7-5ba024c16ecb> in <module>
    ----> 1 s.reindex(labels)
    
    /usr/local/lib/python3.7/site-packages/pandas/core/series.py in reindex(self, index, **kwargs)
       4028     @Appender(generic.NDFrame.reindex.__doc__)
       4029     def reindex(self, index=None, **kwargs):
    -> 4030         return super().reindex(index=index, **kwargs)
       4031 
       4032     def drop(
    /usr/local/lib/python3.7/site-packages/pandas/core/generic.py in reindex(self, *args, **kwargs)
       4542         # perform the reindex on the axes
       4543         return self._reindex_axes(
    -> 4544             axes, level, limit, tolerance, method, fill_value, copy
       4545         ).__finalize__(self)
       4546 
    /usr/local/lib/python3.7/site-packages/pandas/core/generic.py in _reindex_axes(self, axes, level, limit, tolerance, method, fill_value, copy)
       4565                 fill_value=fill_value,
       4566                 copy=copy,
    -> 4567                 allow_dups=False,
       4568             )
       4569 
    /usr/local/lib/python3.7/site-packages/pandas/core/generic.py in _reindex_with_indexers(self, reindexers, fill_value, copy, allow_dups)
       4611                 fill_value=fill_value,
       4612                 allow_dups=allow_dups,
    -> 4613                 copy=copy,
       4614             )
       4615 
    /usr/local/lib/python3.7/site-packages/pandas/core/internals/managers.py in reindex_indexer(self, new_axis, indexer, axis, fill_value, allow_dups, copy)
       1249         # some axes don't allow reindexing with dups
       1250         if not allow_dups:
    -> 1251             self.axes[axis]._can_reindex(indexer)
       1252 
       1253         if axis >= self.ndim:
    /usr/local/lib/python3.7/site-packages/pandas/core/indexes/base.py in _can_reindex(self, indexer)
       3097         # trying to reindex on an axis with duplicates
       3098         if not self.is_unique and len(indexer):
    -> 3099             raise ValueError("cannot reindex from a duplicate axis")
       3100 
       3101     def reindex(self, target, method=None, level=None, limit=None, tolerance=None):
    ValueError: cannot reindex from a duplicate axis

labelには重複したindexである'a'が含まれていないが、それでもエラーになっているんだな。

「indexが'a'の箇所が複数あるので、どの'a'を使ったら良いかわからないよ!」というくらいの意味だろうか。

joinでエラー発生……しなかった

以降の話はpandasの公式ドキュメントに書いていない。書いていないけど、reindex()関数以外でも ValueError: cannot reindex from a duplicate axisが発生するケースはいくつかあるようだ。

https://stackoverflow.com/questions/27236275/what-does-valueerror-cannot-reindex-from-a-duplicate-axis-mean

を見ると、データフレームのjoinでエラーが発生すると書いてあった。

https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.join.html

ちょっと手元で色々試してみたけど、結果的にはjoinでこのエラーを発生させることはできなかった。

普通のjoin

pandasのコミッターであるsinhrksさんの記事がとても秀逸なので、サンプルコードをお借りいたします。 DataFrameを連結・結合する処理で困ったらここを見ましょう。
http://sinhrks.hatenablog.com/entry/2015/01/28/073327

left = pd.DataFrame({'A': ['A0', 'A1', 'A2'],
                     'B': ['B0', 'B1', 'B2']},
                    index=['K0', 'K1', 'K2'])
left

# --------------------

     A   B
K0  A0  B0
K1  A1  B1
K2  A2  B2
right = pd.DataFrame({'C': ['C0', 'C2', 'C3'],
                      'D': ['D0', 'D2', 'D3']},
                     index=['K0', 'K2', 'K3'])
right

# --------------------

     C   D
K0  C0  D0
K2  C2  D2
K3  C3  D3
left.join(right)

# --------------------

     A   B    C    D
K0  A0  B0   C0   D0
K1  A1  B1  NaN  NaN
K2  A2  B2   C2   D2

joinは(何も指定しなければ)2つのDataFrameのindexに基づいて、データを結合する。

indexが重複したDataFrameをjoin→エラーにならない……

問題はここからだ。 rightのindexを重複させます。

right_dup_1 = pd.DataFrame({'C': ['C0', 'C2', 'C3'],
                      'D': ['D0', 'D2', 'D3']},
                     index=['K0', 'K2', 'K2'])
right_dup_1

# --------------------

     C   D
K0  C0  D0
K2  C2  D2
K2  C3  D3

これでエラーになるか?

left.join(right_dup_1)

# --------------------

     A   B    C    D
K0  A0  B0   C0   D0
K1  A1  B1  NaN  NaN
K2  A2  B2   C2   D2
K2  A2  B2   C3   D3

エラーにはならなかった。   なるほど。rightにはindexが'K2'の行が2つあるから、leftのK2をrightの2行のそれぞれとjoinした結果になるのね。

……その他、以下のような場合を試してみたが、全然エラーにならなかった。

  • leftのindexが重複している
  • rightのindexが重複している
  • leftとrightのindexが重複している
  • 複数dtypeのDetaFrameにしてみる
  • how='inner'を指定してみる

うーーん、分からない。 indexが重複したDataFrameを使ってjoinしてみたけど、エラーが再現しなかった。
どこかのアップデートでjoinの仕様が変わったってことかもしれない?

新しい行/列の割当て(assign)でエラー発生

これはQiitaに説明があったので、そちらを参照してください。 手元のpandas 1.0.1で試してもエラーが再現しました。良かった。(良かったのか?)

https://qiita.com/waterada/items/c239a6d0424537cfcfb9

その他?

https://stackoverflow.com/questions/30788061/valueerror-cannot-reindex-from-a-duplicate-axis-using-isin-with-pandas

では以下のように書かれている。

The error ValueError: cannot reindex from a duplicate axis is one of these very very cryptic pandas errors which simply does not tell you what the error is.
The error is often related to two columns being named the same either before or after (internally in) the operation.

拙訳:ValueError: cannot reindex from a duplicate axis というエラーは、とっっっても分かりにくいpandasのエラーの一つです。何が誤りだったのか全く教えてくれません。
このエラーは大抵の場合、操作の前や後で同じ名前がついた2つの列があるときに発生します。
(訳注:internally inはよく分からなかったので訳を飛ばしました)

結構頑張って検索しても、どういうときにエラーが発生するのかいまいちよく分からなかった。
reindex関数を明示的に使わなくても(おそらく内部でreindex関数が走って)エラーが上がることがあるから、 「reindex? なんじゃ、そりゃ」となるかもしれない。

参考資料

https://stackoverflow.com/questions/27236275/what-does-valueerror-cannot-reindex-from-a-duplicate-axis-mean

pandasのissueでこのエラー文章を含むものも多数報告されているので、それを見てみるのも良いだろう:
https://github.com/pandas-dev/pandas/issues?q=is%3Aissue+%22cannot+reindex+from+a+duplicate+axis%22
Bugというタグが付いているissueも多い。このエラーに出くわしたら、pandasのバグを踏んでしまった可能性もあるようだ。

(ここまで読んだ人へ:もし自分のエラーがここまで書いてきた内容に該当しないようなら、twitterの@Linus_MK(筆者)にお知らせください。 必要に応じて加筆修正します。)

それでは。

NumPyのarrayとndarrayの違いを調べた

numpy.arrayとnumpy.ndarray、どちらもたまに見かけるのだが、あまり区別がついていなかった。arrayとndarrayの違いを調べてまとめた。 「numpy array ndarray difference」で検索すると出てくるStack Overflowの質問の内容を主に、検証結果をまとめる。

numpy.arrayとnumpy.ndarrayの違いを一言でいうと

2つの違いを一言でいうと、こうなる。

  • numpy.ndarrayはデータ型(正確にはpythonのクラス)である
  • numpy.arrayは、numpy.ndarrayを作成するための関数である

2つをごっちゃにしてたけど、別物だった。
なので自分で使うのはarray。typeを調べた結果として登場するのがndarray。という違いである。
以下、具体例を使って細かく見ていく。

両者の公式ドキュメントは以下。
numpy.ndarray — NumPy v1.18 Manual
numpy.array — NumPy v1.18 Manual

numpy.arrayとnumpy.ndarrayの簡単な例

import numpy as np
import pandas as pd
pd.options.display.notebook_repr_html = False  # jupyter notebook上での出力形式を制御するために書いています。無くても動きます。
x = np.array([1, 2, 3])
x

# --------------------

array([1, 2, 3])
type(x)

# --------------------

numpy.ndarray
isinstance(x, np.ndarray)

# --------------------

True

pythonの標準リストをもとに、numpy.array関数を使ってxを作った。
type()を使ってどのクラスに属しているかを表示している。
また、isinstance関数で、xがnumpy.ndarrayクラスのインスタンスであることを確認している。

xを表示したときに登場するarrayとは何ものなのか?

上記でxを表示するとarray([1, 2, 3])と表示される。このarrayは何なんだろうか。
[1, 2, 3]と単に書いてあったらpythonの標準リストになってしまうので、区別をつけるために必要なんだろうけど。
でもこのarrayはnumpy.array関数とは別物だよね。arrayとは何だろうか。その答えはnumpyの用語集に書いてある。

https://numpy.org/doc/stable/glossary.html

array
A homogeneous container of numerical elements. Each element in the array occupies a fixed amount of memory (hence homogeneous), and can be a numerical element of a single type (such as float, int or complex) or a combination (such as (float, int, float)). Each array has an associated data-type (or dtype), which describes the numerical type of its elements:
DeepL翻訳して修正:数値要素の同質な入れ物。arrayの中の各要素は一定量のメモリを占有します(そのため同質です)。 各要素は単一の型(float, int, complex など)の数値要素であっても、その組み合わせ( (float, int, float) など)であっても構いません。それぞれのarrayには,その要素の数値型を表すデータ型(別名 dtype)が関連付けられています。

Glossary(用語集)に載っている、numpyの「用語」である。
numpy.array()関数と紛らわしいので、正確を期すために以降では「array(numpy用語)」と書くことにする。言葉の使い方に気をつけつつ、上記の操作を説明すると、以下のようになる。

  • pythonのlistから、numpy.array関数を使って、xを作りました。
  • xはarray(numpy用語)です。
  • xはnumpy.ndarrayクラスのインスタンスです。

※ 「array(numpy用語)」というくどい書き方をする人はあまりいないだろうと思って、参考書ではどう書いているのか調べてみた。「NumPy配列」という書き方でした。

2.2 NumPy配列の基礎
Pythonデータサイエンスハンドブック p.41

本書で「配列」、「NumPy配列」、あるいは「ndarray」という言葉が出てきた場合、ほぼ例外なくndarrayオブジェクトを指すものと考えてください。
Pythonによるデータ分析入門 p.96

というわけで、「Numpy配列」と「array(numpy用語)」は同じことである。以下のように言うことも可能だ。

  • xはNumPy配列です。

pythonのリストやpandasのSeries/dataframeからNumPy配列にしたいんだけど?

(arrayかndarrayのどちらかを使うなら)arrayを使う。できあがったものはnumpy.ndarrayクラスのインスタンスになる。

x = np.array([[1, 2], [3, 4]])
x

# --------------------

array([[1, 2],
       [3, 4]])
df = pd.DataFrame({
    'col_A': [1 ,3, 5],
    'col_B': [9, 7, 5],
    'col_C': [111, 222, 333],
})
df

# --------------------

   col_A  col_B  col_C
0      1      9    111
1      3      7    222
2      5      5    333
x = np.array(df)
x

# --------------------

array([[  1,   9, 111],
       [  3,   7, 222],
       [  5,   5, 333]])

だけど、ndarrayで配列を作ることも出来るよね?

できる。numpy.ndarray()と書けば、numpy.ndarrayクラスのコンストラクタを呼び出すことになるからだ。 しかし、推奨されていない方法である。一般的には使わないほうがよいだろう。

Arrays should be constructed using array, zeros or empty (refer to the See Also section below). The parameters given here refer to a low-level method (ndarray(…)) for instantiating an array.
拙訳:array(numpy用語)を作るときは、array, zeros, empty関数を使うべきです(下記のSee Alsoセクションも参照)。ここで(=ndarrayのコンストラクタで)与えられたパラメータは、array(numpy用語)を初期化するために、低レベルのメソッドndarray(…)を参照する。 https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html

numpy.ndarray()の第一引数には、サイズをタプルで指定する。そうするとそのサイズの配列が作成される。

下記に、2行3列の2次元配列の例を示す。

np.ndarray((2, 3))

# --------------------

array([[0., 0., 0.],
       [0., 0., 0.]])

下記に、サイズ2×4×3の3次元配列の例を示す。

np.ndarray((2, 4, 3))

# --------------------

array([[[-3.10503618e+231,  1.73060162e-077,  6.91691904e-323],
        [ 0.00000000e+000,  0.00000000e+000,  0.00000000e+000],
        [ 0.00000000e+000,  0.00000000e+000,  0.00000000e+000],
        [ 0.00000000e+000,  0.00000000e+000,  0.00000000e+000]],

       [[ 0.00000000e+000,  0.00000000e+000, -3.10503618e+231],
        [-3.10503618e+231, -3.10503618e+231, -3.10503618e+231],
        [ 2.96439388e-323,  0.00000000e+000,  0.00000000e+000],
        [ 0.00000000e+000, -3.10503618e+231, -3.10503618e+231]]])

注意:値は初期化されず、ランダムな値になる。(メモリを確保して全く初期化せず、その値を表示している、ように推測される。)したがって実行しても上記と違う値になる可能性がある。

コンストラクタを呼び出すときに、他のリストなどから値を取り込む方法もあるらしい。 しかし、そんなマニアックな方法でnumpy配列を作る人は稀だろうから、ここでは紹介しない。重要なのは、上記の方法は普通は推奨されない方法だ、ということだ。

arrayと書くべきところで、間違ってndarrayを使ってしまった例

arrayを使うべきところでndarrayを書くと、エラーが返ったり変な結果になる。例を示す。

# x = np.array([1, 2, 3]) が正しいのに
x = np.ndarray([1, 2, 3])
x

# --------------------

array([[[0., 0., 0.],
        [0., 0., 0.]]])

はい。普通は使わないコンストラクタのやり方を上記で説明した甲斐がありましたね。 意図せずにnumpy.ndarrayクラスのコンストラクタを呼び出してしまっている。 その結果、サイズが1×2×3の3次元配列ができて、それに不定の値が入ったものが返っている。

# x = np.array(range(100)) が正しいのに
x = np.ndarray(range(100))

# --------------------

---------------------------------------------------------------------------
    ValueError                                Traceback (most recent call last)
    <ipython-input-12-49769a0574ac> in <module>
          1 # x = np.array(range(100)) が正しいのに
    ----> 2 x = np.ndarray(range(100))
    
    ValueError: maximum supported dimension for an ndarray is 32, found 100

1次元で長さ100の配列を作るつもりでこのように書くと、100次元の配列を作ることになってしまう。 NumPyで作れるarray(numpy用語)は32次元までなので、ValueErrorが発生している。

# x = np.array([1.2, 3.4]) が正しいのに
x = np.ndarray([1.2, 3.4])

# --------------------

---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-13-8dca5cc9ca48> in <module>
          1 # x = np.array([1.2, 3.4]) が正しいのに
    ----> 2 x = np.ndarray([1.2, 3.4])
    
    TypeError: 'float' object cannot be interpreted as an integer

また当然、整数でない値を入れるとエラーになる。長さが小数というNumPy配列は作れないからだ。

ndarrayと書くべきところで、間違ってarrayを使ってしまった例

x = np.array([1, 2, 3])
# isinstance(x, np.ndarray) が正しいのに
isinstance(x, np.array)

# --------------------

---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-14-e2e9a9c75584> in <module>
          2 # isinstance(x, np.ndarray) が正しいのに
          3 
    ----> 4 isinstance(x, np.array)
    
    TypeError: isinstance() arg 2 must be a type or tuple of types

こっちは簡単ですね。isinstanceの2番めの引数はtypeじゃなきゃいけない(のに、typeじゃなくて関数を入れてますよ)、というエラーになる。

参考資料

Python - arrayとndarrayの違い|teratail

python - What is the difference between ndarray and array in numpy? - Stack Overflow

それでは。

pandasのDataFrameに空の列を追加する

pandasのDataFrameに、空の列を新しく追加(挿入)したい場合。

空の列の位置は気にしない場合(右端になる)

既存のDataFrameに1つの列を新規に追加するには、df['new_column'] = (追加したい値)とすればよい。
このとき、好きな値を選べば、空の列ができる。
なお、「空の列」を作るときにこの値を選べ、という唯一の正解は存在しない(と思う)。
以下、実際の例で説明する。

https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.isnull.html

import pandas as pd
import numpy as np
pd.options.display.notebook_repr_html = False  # jupyter notebook上での出力形式を制御するために書いています。無くても動きます。
# 動作環境の確認
print(pd.__version__)
print(np.__version__)
# --------------------
1.0.1
1.18.1
df = pd.DataFrame({'col_A': [1,2,3], 'col_B': ['p','q','r']})
df
# --------------------
   col_A col_B
0      1     p
1      2     q
2      3     r

適当なDataFrameを作成した。これに対して、空の文字列を指定して、新たな行を追加する。

df['new_col_1'] = ''
df
# --------------------
   col_A col_B new_col_1
0      1     p          
1      2     q          
2      3     r          

次はPythonのNoneを指定してみる。

df['new_col_2'] = None
df
# --------------------
   col_A col_B new_col_1 new_col_2
0      1     p                None
1      2     q                None
2      3     r                None

NumPyのNaNを指定してみる。

df['new_col_3'] = np.nan
df
# --------------------
   col_A col_B new_col_1 new_col_2  new_col_3
0      1     p                None        NaN
1      2     q                None        NaN
2      3     r                None        NaN

pandasのNAを指定してみる。
pd.NAはpandas 1.0.0から追加された新しいNaNです。詳しくはこちらをどうぞ。
pandas 1.0.0 (rc0) での pd.NA の特徴 - Qiita
pandas最新バージョンの、pandas1.0について紹介します - Python学習チャンネル by PyQ

df['new_col_4'] = pd.NA
df
# --------------------
   col_A col_B new_col_1 new_col_2  new_col_3 new_col_4
0      1     p                None        NaN      <NA>
1      2     q                None        NaN      <NA>
2      3     r                None        NaN      <NA>

以上のようにして、空の列を新規追加することができる。

どの値を入れれば良いの?

色々な値を指定して空の列を作れる。「え、そうはいっても、どれを使えば良いの?」という人がいるかも知れない。 「空の列」という定義が一意に決まっているわけではないから、好みに応じて作れば良いと思う。
ここからは事実じゃなくて個人の意見になるのですが。
空の列を作ること自体が目的というわけではなく、空の列を作ってからそこに値を埋めていきたいのだろう。 その値のデータ型(dtype)に応じた値を作れば良いと思う。
(目的に合わないdtypeで列を作ってしまうと、意図しない動作につながる可能性があるため。)

dtypeについてはこちらも参照:

linus-mk.hatenablog.com

df.dtypes
# --------------------
col_A          int64
col_B         object
new_col_1     object
new_col_2     object
new_col_3    float64
new_col_4     object
dtype: object

へぇ、pd.NAを単独で指定すると、そのcolumnのdtypeはobjectになるんだな。

作ろうとしている列のデータ型がobject型なら空文字''None浮動小数点ならnp.NaNが良さそう。整数型の場合は……欠損を表す特別な整数値を指定して作るのが良いんじゃないか?

df.isnull()
# --------------------
   col_A  col_B  new_col_1  new_col_2  new_col_3  new_col_4
0  False  False      False       True       True       True
1  False  False      False       True       True       True
2  False  False      False       True       True       True

ちなみに、isnull()関数を適用すると、空文字列はFalseに、Noneとnp.NaNとpd.NAはTrueになる。

空の列を指定した位置に挿入したい場合

右端ではなく、指定した位置に新たな列を挿入したい場合はinsert()を使う。 こちらも参照。

linus-mk.hatenablog.com

下記に例を示す。

df = pd.DataFrame({'col_A': [1,2,3], 'col_B': ['p','q','r'], 'col_C': [1.2,3.4,5.6]})
df
# --------------------
   col_A col_B  col_C
0      1     p    1.2
1      2     q    3.4
2      3     r    5.6
# col_Bとcol_Cの間に新しく列を追加する。最初の引数に位置を示す2を指定する
df.insert(2, 'new_col', np.NaN)
df
# --------------------
   col_A col_B  new_col  col_C
0      1     p      NaN    1.2
1      2     q      NaN    3.4
2      3     r      NaN    5.6

注意事項

と、ここまで書いてきたが、注意事項がある。空の列を新規追加するそもそもの理由についての問題だ。

すでに書いたが、空の列を作ってそれで満足という人はおそらくいないだろう。 空の列自体が目的というわけではなく、空の列を作ってからそこに何らかの値を埋めていきたいのだろう。

どうやって埋めていくのか。1つずつ値を計算する? もしそうなら、それはpandasが苦手な動作だ。 ループを用いると、処理がとても遅くなる。 特にDataFrameが大きい場合には、他の方法を考えたほうが良いかもしれない。

それでは。

参考

python - How to add an empty column to a dataframe? - Stack Overflow

jupyter notebookのサンプルコードをブログに貼り付ける方法を調べた

このブログを書くときには、jupyter notebookでサンプルのコードを書いて、pythonや各種ライブラリの動作を検証している。

そこでいつも頭を悩ませるのが、「jupyter notebookの上のサンプルコードを、どうやってはてなブログに貼り付けるか?」である。

毎回困っているので、今回は一つ、同じソースコードを色々な方法ではてなブログに挿入してみて、どれが良いか比較してみよう。
なお、現時点ではpandasを使ったデータ処理の説明をすることが多いので、 「DataFrameやSeriesをうまく表示できること」を重視する。
(matplotlib/seabornなどの解説で、図を入れるようになったら、また話が変わってくるのかもしれない。)

f:id:soratokimitonoaidani:20200524133015p:plain 画像はhttps://unsplash.com/photos/Ys-DBJeX0nEより

スクリーンショット(画面キャプチャ)で保存

スクリーンショット(画面キャプチャ)で画像を撮って貼り付ける方法だと、以下のようになる。 はてなブログの記事編集画面から、該当箇所に画像を挿入すればよい。

f:id:soratokimitonoaidani:20200524133120p:plain

ついでにいうと、この記事では上記のjupyter notebookを例に説明する。
中のコードに大した意味はない。[pandas]特定の条件を満たす行を削除する - 子供の落書き帳 Renaissanceで書いたコードを少し改変して、pandasのDataFrameとSeries(行・列)、単一の要素を表示するようにしただけだ。

メリットとデメリット

markdownで保存

jupyter notebookの「File→Download as」の中にはmarkdown書式がある。 はてなブログをいつもmarkdown記法で書いているから、そのまま貼り付ければうまく書式設定されるはずである。やってみよう。

import pandas as pd
df = pd.DataFrame({
    'name'    : ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Fred'],
    'English' : [12, 34, 56, 78, -1, 90],
    'Math'    : [88, 66, -1, 44, 22, -1]    
})
df
name English Math
0 Alice 12 88
1 Bob 34 66
2 Charlie 56 -1
3 David 78 44
4 Eve -1 22
5 Fred 90 -1
df.loc[4, 'English'] = 99999
df.loc[4]
name         Eve
English    99999
Math          22
Name: 4, dtype: object
df['English']
0       12
1       34
2       56
3       78
4    99999
5       90
Name: English, dtype: int64
df.loc[3, 'Math']
44

markdownで保存(ただしPandas.DataFrameの表組みはテキスト表示)

jupyter notebook上で、Pandas.DataFrameの表組みをHTMLで表示するかプレーンテキストで表示するかは、設定で変更できる。
pd.options.display.notebook_repr_html = Trueと書くと、Pandas.DataFrameをきれいにHTMLで表示する(デフォルト)。
pd.options.display.notebook_repr_html = Falseと書くと、Pandas.DataFrameをテキストで表示する。

参考:
[https://note.nkmk.me/python-pandas-option-display/:title]
[https://qiita.com/tanemaki/items/2ed05e258ef4c9e6caac:title]

notebook内にこのコマンドを書いてmarkdown保存した場合は、以下のようになる。

import pandas as pd
pd.options.display.notebook_repr_html = False
df = pd.DataFrame({
    'name'    : ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Fred'],
    'English' : [12, 34, 56, 78, -1, 90],
    'Math'    : [88, 66, -1, 44, 22, -1]    
})
df
      name  English  Math
0    Alice       12    88
1      Bob       34    66
2  Charlie       56    -1
3    David       78    44
4      Eve       -1    22
5     Fred       90    -1

(以降は通常のmarkdown保存と変わらないので省略)

メリットとデメリット

  • メリット
    • 手間がかからない。簡単に見栄えのよい物が作れる。
  • デメリット
    • 入力したコードと実行結果との区別がつきにくい。実行結果が無い(コード入力だけの)セルが続いたときに、どれが入力すべきコードでどれが結果かわからなくなるだろう。
    • 入力したコードと実行結果が一緒になっていない。できれば同じコードブロックの中でコードと実行結果がセットになっているのが一番分かりやすいと思う。
    • 表の表示がイマイチ。何で右端に境界線が出現するの?
      • この問題はプレーンテキストで表示すると解消できる

jupyter notebookを手でコピーペーストして色々いじる

手でコピーペーストしようとしてブラウザ上で「すべて選択」→「コピー」をしても、なぜか入力コード部分だけで、実効結果はコピーされないのよね……。
したがって、出力部分は別途、一つ一つ選択してコピーしてペーストした。

今回は入力コードと出力結果を# ------で区切ることにした。もちろん、別のルールを決めてもよい。

(なお、ブラウザ上のjupyter notebookを元にしたが、markdownをダウンロードして、それを元に手で修正することもできる。)

import pandas as pd
pd.options.display.notebook_repr_html = False  # DataFrameの見栄えをよくするために
df = pd.DataFrame({
    'name'    : ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Fred'],
    'English' : [12, 34, 56, 78, -1, 90],
    'Math'    : [88, 66, -1, 44, 22, -1]    
})
df
# ------
      name  English  Math
0    Alice       12    88
1      Bob       34    66
2  Charlie       56    -1
3    David       78    44
4      Eve       -1    22
5     Fred       90    -1
df.loc[4, 'English'] = 99999
df.loc[4]
# ------
name         Eve
English    99999
Math          22
Name: 4, dtype: object
df['English']
# ------
0       12
1       34
2       56
3       78
4    99999
5       90
Name: English, dtype: int64
df.loc[3, 'Math']
# ------
44

メリットとデメリット

  • メリット
    • 自由度が高い。自分が一番良いと思うスタイルで書ける。
      • 例えば、DataFrameをmarkdownの表組みで書きたければ、その方式で書ける。
    • 入力したコードと実行結果がセットで一緒にできて、対応関係がわかりやすい。
  • デメリット
    • 手間が増える。時間がかかる。面倒。特にコードが多いと労力がかかって辛い。

gistにして貼り付ける

この方法は自分はやったことなかったけど、「jupyter notebook はてなブログ」で検索すると、この方法でやってる人もいるみたい。

詳しい方法については、Jupyter Notebookをはてなブログに貼り付ける方法 - akatak’s blogなどを参照。この記事に従ってgistを作って、それをはてなブログから読み込むと、以下のようになる。

jupyter notebookのサンプルコードをブログに貼り付ける方法を調べた

メリットとデメリット

  • メリット
    • 手間がかからない。簡単に見栄えのよい物が作れる。
  • デメリット
    • スクロールしないと全部見えないので一覧性が悪い。
      • 裏を返せば、コードがだらだら縦に長くならないというメリットでもある。

その他

その他の選択肢

  • 「htmlで保存(File→Download as→HTML (.html))」はダメだ。確かに保存したHTMLファイルを開くときれいにもとのコードが表示される。しかしHTMLファイル内には超大量のCSSがあって、そのおかげでもとの体裁が保持されているのである。これを全部ブログに貼り付けるというのは到底現実的ではない。
  • pythonファイルで保存(File→Download as→Python (.py))」もダメだ。これはPythonスクリプトファイルとして実行できるように保存するものである。コードを実行した結果の表示が全く無いので、今回の目的には不向きである。

他のドキュメントとか

  • scikit-learnの公式サイトのサンプルコードは、コードの状態が切り替えられるようになっている。プロンプト(左端の>>>)と出力とを、見える状態と見えない状態にできるので、入力だけコピーしてすぐに自分の環境で実行できる。すごい。しかし俺には真似できない。

結論

うーん……どれも一長一短だわ……
今までその場その場で適当な方法で記事のコードを作っていたが、 今回改めて各方法を調べてみた。 これが最高という方法が無いということは、よく分かった。
markdownファイルを入力すると、自分が最適だと思うスタイルのコードが出力されるようなスクリプトを作るのが一番良い気がしてきた。「jupyter notebookを手でコピーペーストして色々いじる」のが面倒なので、退屈なことはPythonにやらせよう、というわけでPythonに整形をやらせる方式ね。)

参考文献

「よいサンプルコード」ってどんなサンプルコード? 〜Qiitaや技術ブログを書くときに気を付けること〜 - Qiita
サンプルコード自体の書き方という記事なので、主題は少し違うものの、少し参考になったので書いておく。
例えば「言語の文法として正しいこと」「嘘のコード(実は結果が違ったり動かなかったりするもコード)」のような話が書いてある。
この記事との関連でいうと、「コードの実行結果も一緒に載せること」「コードやエラーメッセージをスクショ(画像)にしないこと」という点が該当する。

pythonの機械学習・データ分析系の各ライブラリの人気度を調べてみる

2万4000超の開発者に聞いた、Pythonでよく使うフレームワークは? | マイナビニュース
https://www.jetbrains.com/lp/python-developers-survey-2019/
を読んで調べたこと。

Python Developers Survey 2019 Results

これは「Python Software FoundationがJetBrainsを使用して実施したPython開発者調査の報告書」らしい。
その中で、各種ライブラリを使っている人の割合(複数回答)が掲載されている。 興味があるデータサイエンス系のフレームワークを見てみよう。ディープラーニングフレームワークは、俺が全然使っていないので、下記の結果一覧から除外した。 正確を期すためにもとのデータを見ると、質問文は「What data science framework(s) do you use in addition to Python?」である。

  • Numpy 63%
  • Pandas 55%
  • Matplotlib 46%
  • Scipy 36%
  • SciKit-Learn 33%
  • Seaborn 17%

まぁ妥当だろう。個人的にはScipyを使うことがあまり多くないので、Scipyってこんなに多いの?と思うけど。
では、他の調査も見てみよう。

Stack Overflow trend検索

Stack Overflowの公式ブログで、Pythonの人気が上昇していることを取り上げた記事がある。 以下に示す2つの記事がそうだ。いずれも2017年に書かれたものである。

このうち1つ目の記事では、高所得の国ではpythonの質問の閲覧数が大きく増えていることが述べられている。 2つ目の記事では、pythonのライブラリ群に焦点を当てていて、pandasが特に急激に成長している事がわかる。(Stack Overflowの中で、該当タグの質問へのトラフィック量を調べたもの)

The Incredible Growth of Python | Stack Overflow
Why is Python Growing So Quickly? - Stack Overflow Blog

だがこれは2017年なので、改めて最新の結果を見てみよう。

まず、言語どうしを比較してみよう。Stack Overflow Trendsの機能を使って、10個の言語を比較するとこのようになる。 (こちらは月間に投稿された質問のうち、該当するタグが含まれている割合を示す)

f:id:soratokimitonoaidani:20200510115224p:plain
Stack Overflow Trends

すごい。
2017年最初には8%くらいだったけど、その後も急激に上がり続けて、JavaScriptを抜いて1位となり、2020年には14〜15%になっている。人気を集めすぎやろ。

次に、pythonの各ライブラリについて同様にグラフを作った。

f:id:soratokimitonoaidani:20200510115247p:plain
Stack Overflow Trends

1位のpandasが2.4%程度で、2位のnumpyが0.8%程度である。2位の約3倍も質問されているpandasの一人勝ちと言ってよいだろう。
Python Developers Survey」では1位がnumpy、少しの差で2位がpandasであった。両者で違う傾向になっている。

Stack Overflow 質問数

次に、Stack Overflowで各ライブラリのタグが付いた質問の数を調べた。(これはある月間ではなく、今までの累計で質問された数である)
ついでにそれらの質問の中で、最も多いvoteの数も調べた。
Highest Voted 'pandas' Questions - Stack Overflow など

  • pandas: 質問数149000、最多vote1909
  • numpy: 質問数74000, 最多vote577
  • matplotlib: 質問数47000, 最多vote1978
  • scikit-learn: 質問数19000, 最多vote213
  • scipy: 質問数16000, 最多vote406
  • seaborn: 質問数4700, 最多vote206

こちらもpandasの一強といって良さそうだ。2位のnumpyの約2倍の質問数だ。
個人的にはきれいな図が書けるseabornが好きなんだけど、質問数が少ないですね、人気がないのかなぁ……ちょっと残念。

Google トレンド

最後にGoogleトレンドを見てみよう。日本からの最近12ヶ月で検索された量を調べると以下のようになった。

https://trends.google.co.jp/trends/explore?geo=JP&q=%2Fm%2F0rphppq,%2Fm%2F021plb,%2Fm%2F08b9rm,%2Fm%2F0h97pvq,seaborn

(これは静的データであり、更新されない)

  • pandas: 81
  • numpy: 58
  • matplotlib: 34
  • scikit-learn: 22
  • seaborn: 5

(seabornのみ「トピック」が選択できなかったので「検索キーワード」で検索した。また5つまでしか選択できなかったのでscipyを除外した)

ちなみに日本からではなく、全世界に切り替えると、以下のようになった。

  • pandas: 87
  • numpy: 42
  • matplotlib: 25
  • scikit-learn: 20
  • seaborn: 8

pandasがnumpyにダブルスコアになった。日本と比べて、世界ではpandasが大人気なのか……?

調べてみた結果を受けて

Python Software Foundationの調査は「使っているか、使っていないか」というアンケートである。 したがって、Stack Overflowに質問を投稿したり、Googleで検索するのとは違った傾向になるだろう。

scikit-learnが、使用率が高いのに、Stack Overflowの質問数やGoogleトレンドだとかなり低いのは何でだろうか。

一つ考えられる仮説は、使う頻度が違うのかなー。 個人的には、pandas, numpyは頻繁に使うけど、scikit-learnはたまにしか使わない。 したがって、使うか使わないかのアンケートに答えるとしたら全て「使う」と回答するけど、 Stack Overflowを見に行く頻度はpandasやnumpyのほうが絶対に多い。
多くのユーザーが同様に、pandasやnumpyの頻度が高くscikit-learnの頻度が低いようなら、上記で見てきた結果と整合する。

もう一つ考えられる仮説は、どれだけみんなが「使い方分からん……検索しよう/人に質問しよう」と思うか、が違うということ。 つまり使う頻度じゃなくて、書いてて詰まる頻度のほうね。 scikit-learnの使い方は比較的統一されていて、基本的にはfitしてpredictしちゃえば良い(注:かなり乱暴な話だが)。 使い方がわからなくてStack Overflowに聞いたりGoogleで検索したりすることが少ない、ということかね。 ……だとすると、pandasはStack OVerflowやGoogleトレンドだと値が大きいから、みんなが「うーん、分からん」と思ってるってことだろうか?

今後自分がどの辺の記事を書けば良いのかな―と思っていたけど、人に来てもらおうと思ったらpandasで書くのが良さそうだ。
それでは。

金成隆一「ルポ トランプ王国」読書感想文 アメリカン・ドリームの終焉

金成隆一「ルポ トランプ王国」(岩波新書)を読んだので、感想文を書く。
何についての本かというと、2016年のアメリカ大統領選挙クリントンとトランプが戦ったときのルポルタージュだ。トランプを支持する市民に多く話を聞き、アメリカの現状を巧みに捉えている。

何で読んだの

トランプ大統領は、選挙戦の始めはただの目立たない候補だったのに、一体どうして大統領選挙に勝ったのかと不思議であった。
2016年にはトランプ大統領が当選し、イギリスのEU離脱投票があった。「まさか」の事態が起きることが続いた。

この本は、数年前に一度図書館で借りたが、途中まで読んだところで期限が来たので返してしまった。 *1ただ、興味深くて全部読みたかったので、改めて本屋で買って(内容を少し忘れていたので)最初から通読した。

筆者は朝日新聞の記者であり、ニューヨークに駐在している。アメリカの各地にでかけて、出会った人々に話しかけてインタビューし、トランプを支持する理由、現在の暮らし向きなどを尋ねた。取材で行った先々の経験が本書の核となっている。
したがって、基本的に「アメリカの国民の個人的な経験」の要素が強いが、要所要所で統計やグラフなどのデータを盛り込み、論拠を補強している。「それは個人の感想ですよね?」という批判にも堪えうる良い構成と言える。

アメリカン・ドリーム

筆者がトランプ支持者にインタビューすると、「現在の暮らしが良くならない」「昔はこんなではなかったのに」という話が多く出てくる。
そこで重要なのが「アメリカン・ドリーム」という概念だ。筆者はこう説明している。

私の理解では、出自がどうであれ、まじめに働いて、節約して暮らせば、親の世代より豊かな暮らしを手に入れられる、今日より明日の暮らしは良くなるという夢だ。(p.210)

また、辞書を引けば、もっと詳しい定義も出てくる。例えば以下の通りだ。

ブリタニカ国際大百科事典 小項目事典の解説

アメリカ人が建国以来信奉してきたアメリカ的成功の夢。より具体的には,機会の平等を通じての経済的成功や物質的繁栄の夢を指すが,その達成の過程にピューリタニズムの伝統に基づく勤勉,節約が存在していることが特徴的である。ベトナム戦争での挫折後,アメリカン・ドリームを過去のものとして語る傾向が強まったが,「強いアメリカ」を掲げたレーガン大統領の登場などは,アメリカン・ドリームに対するアメリカ人の捨て切れぬ思いを如実に物語るものであった。
https://kotobank.jp/word/%E3%82%A2%E3%83%A1%E3%83%AA%E3%82%AB%E3%83%B3%E3%83%BB%E3%83%89%E3%83%AA%E3%83%BC%E3%83%A0-158170

が、私自身はアメリカの地を踏んだことさえ無いので、アメリカの人々が「アメリカン・ドリーム」に抱いている思い、ニュアンスが理解できていない。
失われた20年だか30年だかに苦しんでいる日本から見れば、「アメリカ人って、その夢を無条件に信じていられたもんなの!?」と疑う気持ちも持ってしまう。
それはともかく、昔のアメリカ人が実現できていたアメリカン・ドリームは、今では崩壊しているらしい。

オハイオ州トランブル郡の共和党委員長は「この一体では、主要産業の衰退、廃業、海外移転、合併など何でも起きた。アメリカン・ドリームを実現する機会はもうない」と話した。
この思いは広く共有されていた。夢を失った地域は活力も失う。ラストベルトやアパラチアでは薬物中毒の死が増えている。(p.211)

平たくいえば、一生懸命真面目に働いても、解雇されたり会社が倒産したり、給料が減ったりして、暮らしは悪くなるばかり、ということだろう。
その中で、「今までの政治家に任せてもダメだ、型破りな新しいトランプに任せてみよう」と考える人もいる。あるいは「アメリカに仕事を取り戻す」というトランプの主張に共感して、トランプを支援する人もいる。

ロバート・パットナムの「われらの子ども」という本があり、これもアメリカの格差社会を鮮やかな筆致で描いた傑作である(まだちゃんと読んでない)。この本の原題は「Our kids: The American Dream in Crisis」であり、訳者は「危機にあるアメリカンドリーム(p.315)」と書いている。
アメリカン・ドリーム、どうやらいま大事な概念らしい。

地理的な分断なのか、社会階層的な二極化なのか

「ルポ トランプ王国」と「われらの子ども」とは、どちらもアメリカのミドルクラスの没落を扱っていて、題材がよく似ている。アメリカン・ドリームの話、薬物汚染の話など、共通点も多い。 ただ、相違点が1つあるようだ。

「ルポ トランプ王国」では、地理的な問題に注目している。「われらの子ども」では、米国の二極化は地域によるというよりも、むしろ収入(階級)によると述べている。

「ルポ トランプ王国」のキーワードは、「アパラチア山脈」「ラストベルト」である。ちょうど上の引用部分でも登場している。著者によれば、ニューヨークにいてもトランプの支持層が全く見つからなかった。それでトランプの支持層を求め、地方に行って探してみたら、トランプを支持する人ばかりであった。それは「普段の取材では見えない、見ていない、もう一つのアメリカ、『トランプ王国』(はじめに p.ii)」だと筆者は語る。

これに対して、「われらの子ども」のパットナムは、地域間の分断を全否定はしていない。しかし、「これはアメリカ全土に広がる問題ですよ」と主張している。訳者の解説がうまくまとめているので、引用する。

まず質的側面、インタビューのパートであるが、例えばこのような格差の問題を産業の衰退したいわゆるラストベルトのプアホワイト、あるいは人種やジェンダーに固有なそれぞれの問題として捉えることは可能であり、そのような議論も少なくないだろう。しかし前掲の表から明確に読み取れるように、この格差が子どもの成長過程をかけ離れたものにする様子は、アウトドアリゾート地や「セレブ」都市、あるいは伝統的な南部、東部の都市など全米のあらゆる地域に広がっていること、また白人だけでなくマイノリティ、さらにはシングルマザー家庭など、それぞれの内部においても同じように乖離が起こっているということが、インタビューの地点と対象を巧みに選定したことによって表現されている。
https://www.sogensha.co.jp/special/ourkids/index.html

この相違点は、どちらかが正しくてどちらかが間違っている、というものではないだろう。(「われらの子ども」では、収入の多い人と少ない人でこう違う、という図表が超大量に出てくる。だが、都市部か地方かは収入の交絡因子になるはずだ。)今後「われらの子ども」を読む際にはその点を注意して読んでいきたい。

象グラフ、アメリカの中間層、日本の中間層

最後の7章では、「アメリカのミドルクラスの悩みは、日本と同じものだ」という話が出てくる。
この中で興味を引いたのが「象グラフ」だ。
ブランコ・ミラノビッチ(Branko Milanovic)が作成したグラフで、世界の人々の所得の上昇率を図持した。

f:id:soratokimitonoaidani:20200429163824j:plain 図はグローバル化、反発する民意:朝日新聞GLOBE+より引用 *2
グラフの説明については米大統領選におけるトランプ氏勝利と「グローバル化の象」 - しいたげられたしいたけ も参照

先進国のミドルクラスはここ数十年、収入がほとんど上がっていないということになる。地球上の他の人に比べて割を食った格好だ。ここには日本人の多くも入るだろう。

アメリカではトランプが当選したし、イギリスではブレグジットが起きた。日本では何が起きるのだろうか?


ちなみに著者の金成隆一は、2019年9月に続編の「ルポ トランプ王国2: ラストベルト再訪 (岩波新書)」を書いている。
それでは。

おまけ:ページを折った箇所

*3

  • p.22 昔は何もなくても良い給料の仕事にありつけた
  • p.42 ブルー・ドッグ 保守的な民主党支持者
  • p.44 街の衰退、薬物汚染
  • p.75 論文 白人中年の死亡率が上昇
  • p.114 ミドルクラスは残っていない。ごく一部が上がり、残りは下層に落ちた
  • p.125 アメリカン・ドリームはもうない。必死に働いても中流の下だ
  • p.156 ブルー・チェック
  • p.164 ニューヨーク・タイムズの記者の英語がこんなに下手なわけ無いでしょ!
  • p.182 アパラチアは白人の街
  • p.198 アメリカン・ドリームの実現が本当に難しくなっている。もう無理よ
  • p.209 アメリカン・ドリームは死んでいた→「アメリカン・ドリーム」の定義など
  • p.231 トランプ発言をファクトチェックしても、メディアを有権者が信用していない
  • p.243 給料の良い仕事がなくなったと嘆く労働者、技能のある労働者が見つからないと嘆く経営者
  • p.246 象グラフ 先進国の中間層は収入が増えていない

*1:図書館のシステムで貸し出し履歴をみたら、2017年9月26日に借りていたと分かった。

*2:ちなみにこの記事は「ルポ トランプ王国」を書いた金成隆一も原稿を書いている。

*3:前回の読書メモではどこが印象に残ったか分からなくなったので、今回は気になるところでページを折ってみたが、目的の場所がすぐに探せたし、どこを重点的に書けばいいか分かったのでとても良かった。

pandas 特定列の値をユニークな数値IDに変換する3つの方法

pandasのDataFrameやSeriesがあったときに、ある列の値に基づいて数値に変換して、ユニークな整数IDを振りたい時がある。文字列の型のカテゴリを番号に変換したいという状況だ。
1行ずつ見ていけばできることはできるのだが、もっと簡単に速くできる方法は無いのか。

以下、StackOverflowや公式ドキュメントを参考に、検証結果をまとめておく。

f:id:soratokimitonoaidani:20200418212154p:plain

問題設定と希望する出力

例として、適当なDataFrameを作成する。

import pandas as pd
df = pd.DataFrame({
    'name'    : ['Alice', 'Bob', 'Charlie', 'Charlie', 'Alice', 'Bob'],
    'item' : ['aaa', 'bbb', 'ccc', 'ddd', 'eee', 'fff'],
    'number'    : [3, 2, 4, 3, 2, 1],
})
df
name item number
0 Alice aaa 3
1 Bob bbb 2
2 Charlie ccc 4
3 Charlie ddd 3
4 Alice eee 2
5 Bob fff 1

このデータは、通販の商品注文なのか、レストランに入って注文してるのか、どういうシチュエーションなんだろう。
まぁ良いや。あんまり考えずに作ったデータなので。
で、nameの値に応じて番号を振りたいとしよう。こんなふうに。

(注意:name_idの順序付けには条件が無いものとする。すなわち、出現順やアルファベット順でなくても良いとする。)

df['name_id'] = [0, 1, 2, 2, 0, 1]
df
name item number name_id
0 Alice aaa 3 0
1 Bob bbb 2 1
2 Charlie ccc 4 2
3 Charlie ddd 3 2
4 Alice eee 2 0
5 Bob fff 1 1

今回は手動で列の値を指定して追加したけど、もちろん実際のデータでこんなことはできない。
これを自動で実行するにはどうすればよいか。
3つの方法があることが分かったので、まとめて書いておく。

(※以下の各方法について説明する前に、最初に書いたdfを作成しているものとして読んでください。)

方法1 factorize()

1つ目の方法はfactorize()関数を使用するものだ。   factorizeという単語の意味は、英和辞典を引くと「因数分解する」と書いてある。しかし、この操作は別に多項式因数分解ではないよな。データ分析だと別の意味があるのだろうか。

This method is useful for obtaining a numeric representation of an array when all that matters is identifying distinct values.
このメソッドは,異なる値を識別することだけが重要な場合に,配列の数値表現を得るのに役立ちます. (DeepL翻訳)
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.factorize.html

同じ値が同じ数字になり、違う値が違う数値になるように、数値に変換するよ、という話なので、今回の目的にピッタリあう。

df = pd.DataFrame({
    'name'    : ['Alice', 'Bob', 'Charlie', 'Charlie', 'Alice', 'Bob'],
    'item' : ['aaa', 'bbb', 'ccc', 'ddd', 'eee', 'fff'],
    'number'    : [3, 2, 4, 3, 2, 1],
})
df['name_id'] = df['name'].factorize()

→エラー(内容はクリックすると展開されます)

    ---------------------------------------------------------------------------

    ValueError                                Traceback (most recent call last)

    <ipython-input-27-52d75a2b1282> in <module>
    ----> 1 df['name_id'] = df['name'].factorize()
    

    /usr/local/lib/python3.7/site-packages/pandas/core/frame.py in __setitem__(self, key, value)
       2936         else:
       2937             # set column
    -> 2938             self._set_item(key, value)
       2939 
       2940     def _setitem_slice(self, key, value):


    /usr/local/lib/python3.7/site-packages/pandas/core/frame.py in _set_item(self, key, value)
       2998 
       2999         self._ensure_valid_index(value)
    -> 3000         value = self._sanitize_column(key, value)
       3001         NDFrame._set_item(self, key, value)
       3002 


    /usr/local/lib/python3.7/site-packages/pandas/core/frame.py in _sanitize_column(self, key, value, broadcast)
       3634 
       3635             # turn me into an ndarray
    -> 3636             value = sanitize_index(value, self.index, copy=False)
       3637             if not isinstance(value, (np.ndarray, Index)):
       3638                 if isinstance(value, list) and len(value) > 0:


    /usr/local/lib/python3.7/site-packages/pandas/core/internals/construction.py in sanitize_index(data, index, copy)
        609 
        610     if len(data) != len(index):
    --> 611         raise ValueError("Length of values does not match length of index")
        612 
        613     if isinstance(data, ABCIndexClass) and not copy:


    ValueError: Length of values does not match length of index

あれ。ValueErrorになってしまった。何でだろう。

df['name'].factorize()
(array([0, 1, 2, 2, 0, 1]), Index(['Alice', 'Bob', 'Charlie'], dtype='object'))
type(df['name'].factorize())
tuple
df['name'].factorize()[0]
array([0, 1, 2, 2, 0, 1])

上記の通り、factorize関数はSeries自体ではなくtupleを返してくる。
tupleの1番目は「Seriesのどの位置に、どの要素が入っているか」を示す数値のndarrayが返る。
2番めは「使われている要素一覧」を示すIndexが返る。
(公式ドキュメントに書いてあるとおりだが。)
なるほど、Seriesを上記の2つに「分解して」返してくるので、factorizeという関数名なのであろう。

というわけで、今回ほしいのはtupleの1番目なので、factorize()したあとに[0]を指定すればよい。正しいコードは以下のようになる。

df['name_id'] = df['name'].factorize()[0]
df
name item number name_id
0 Alice aaa 3 0
1 Bob bbb 2 1
2 Charlie ccc 4 2
3 Charlie ddd 3 2
4 Alice eee 2 0
5 Bob fff 1 1

方法2 df.groupby(['column_name']).ngroup()

2番めの方法はgroupby関数を使うものだ。

df['name_id'] = df.groupby(['name']).ngroup()
df
name item number name_id
0 Alice aaa 3 0
1 Bob bbb 2 1
2 Charlie ccc 4 2
3 Charlie ddd 3 2
4 Alice eee 2 0
5 Bob fff 1 1

pandas.core.groupby.GroupBy.ngroupのドキュメントはこちら。(関数の正式名称、長いな!)

pandas.core.groupby.GroupBy.ngroup — pandas 1.0.3 documentation

groupの番号を返す関数である。 groupbyを使って特定列の値でグループ化して、その番号を取ることで、ユニークなIDを付与している。わかりやすい。

方法3 Series.astype('category').cat.codes

3番目の方法は、astypeを使ってcategory型に変換して、その番号を取得するものだ。

df['name_id'] = df['name'].astype('category').cat.codes
df
name item number name_id
0 Alice aaa 3 0
1 Bob bbb 2 1
2 Charlie ccc 4 2
3 Charlie ddd 3 2
4 Alice eee 2 0
5 Bob fff 1 1
df['name'].astype('category').cat.codes
0    0
1    1
2    2
3    2
4    0
5    1
dtype: int8

確かにできているけど、何でこの方法で実現できるのか分からなかった。ので、調べた。

df['name']
0      Alice
1        Bob
2    Charlie
3    Charlie
4      Alice
5        Bob
Name: name, dtype: object
df['name'].astype('category')
0      Alice
1        Bob
2    Charlie
3    Charlie
4      Alice
5        Bob
Name: name, dtype: category
Categories (3, object): [Alice, Bob, Charlie]

astype()を使ってdtypeを変換しているので、dtypeがobjectからcategoryに変わっている。 dtypeについては以下も参照。

linus-mk.hatenablog.com

categoryとはどういうdtypeなのか? を調べようとしたけど、 公式ドキュメントを見ても長い説明が書いてあったので諦めた。
Categorical data — pandas 1.0.3 documentation

カテゴリ型のSeriesはcatというアクセサを通じて色々な情報を取得できる、らしい。すなわち、Series.cat.xxxx という属性で、そのカテゴリ型の情報を取得できる。
Pythonデータ分析/機械学習のための基本コーディング! pandasライブラリ活用入門 p.159)
pandas公式ドキュメントだと多分ここかな。
https://pandas.pydata.org/pandas-docs/stable/reference/series.html#api-series-cat

その中で、カテゴリのIDを取得するには、Series.cat.codesとすれば良いようだ。

関連質問

関連質問:

python - Pandas: convert categories to numbers - Stack Overflow
一番閲覧数が多いのはこれみたいだ。1つの列を基準にカテゴリーを数値に変換する。(ただし1始まりで番号をふろうとしていることだけ注意)

python - Q: [Pandas] How to efficiently assign unique ID to individuals with multiple entries based on name in very large df - Stack Overflow
FirstName列とLastName列という2つの列を連結した値に対して、ユニークな数値IDを付与したいという質問。

この2つを見れば用は足りるだろうと思うが、同様の質問がまだあったので載せておく。

python - Factorize a column of strings in pandas - Stack Overflow

python - Convert pandas series from string to unique int ids - Stack Overflow

それでは。