pandasのデータ型、dtypeについて 公式ドキュメントを翻訳した

この記事は何?公式ドキュメントの(個人的な)翻訳だよ

Essential basic functionality — pandas 1.0.3 documentation

pandasの型であるdtypeについての公式ドキュメントを翻訳した。
内容は、pandasのSeriesやDataframeに関して

  • dtypeの基本事項
  • 型を判定・確認する方法、
  • 他の型と結合した時のアップキャスト
  • astype()関数を使って指定した型に変更・変換する方法
  • infer_objects()、to_numeric()関数を使って指定した型に変更・変換する方法
  • 落とし穴(nanが入ると勝手にdtypeが変わってしまう仕様について)

などである。
(元は公式ドキュメントだが、この翻訳は誰かが公認したわけではなく、個人的・非公式のものである。念のため。)

f:id:soratokimitonoaidani:20191123180503p:plain

参考になる書籍が少ないよ

pandasを使ってデータ処理をしたいときに、データの型を操作することが主目的だという場合は少ないだろう。 しかし、型システムは根幹の部分で必ず使われているため、型をどう扱っているかの理解が必要になる場合もある。 pandasを使う上で避けて通れない話だが、文献を見ても意外と型の記述は少ない。

pandasを作ったWes McKinneyが書いた「Pythonによるデータ分析入門 第2版 ―NumPy、pandasを使ったデータ処理」にも、dtypeの話はあまり載っていなかった。 インプレス社の「Pythonデータ分析/機械学習のための基本コーディング! pandasライブラリ活用入門」の第7章「データ型の概要と変換」では、まるまるdtypeの話をしている。 pp150〜159の10ページあり、私が見た中で最も分量の多い記述である。

書籍を見てもまとまった記述は少ないし、pandas公式ドキュメントを参考にするのが一番だろう。 ということで、公式ドキュメントの該当部分を翻訳した。


dtypes

pandasはほとんどの部分において、Seriesと、DataFrameの個々の列に対して、NumPyのarrayとdtypeを使用している。 NumPyはfloat, int, bool, timedelta64[ns] and datetime64[ns]をサポートしている。 (NumPyは、タイムゾーンの区別のあるdatetimeをサポートしないことに注意。)

pandasやサードパーティのライブラリは、いくつかの点においてNumPyの型システムを拡張している。 このセクションでは、pandasの内部で実施されている拡張について説明する。 pandas上で動作する、自作の拡張を書く方法については、[拡張データ型]を参照のこと。 データ型の拡張をしたサードパーティ製ライブラリの一覧については、[拡張データ型]を参照のこと。

以下の表ではpandasの拡張型を全て列挙している。それぞれの型についての詳細な説明は、各ドキュメントを参照のこと。

Kind of Data Data Type   Scalar  Array   Documentation
tz-aware datetime   DatetimeTZDtype Timestamp   arrays.DatetimeArray    Time zone handling
Categorical CategoricalDtype    (none)  Categorical Categorical data
period (time spans) PeriodDtype Period  arrays.PeriodArray  Time span representation
sparse  SparseDtype (none)  arrays.SparseArray  Sparse data structures
intervals   IntervalDtype   Interval    arrays.IntervalArray    IntervalIndex
nullable integer    Int64Dtype, … (none)  arrays.IntegerArray Nullable integer data type

pandasは文字列を格納するのに、objectというdtypeを用いる。

最後に、任意のオブジェクトは、object dtypeを使えば格納することができる。 しかしこのやり方は可能な限り避けるべきである。 (パフォーマンスが悪くなるため、そして、他のライブラリやメソッドとの相互運用性のためである。オブジェクトの変換 の節を参照。)

DataFrameにはdtypesという便利な属性があり、dtypesは各行のデータ型が格納されたSeriesを返す。

In [328]: dft = pd.DataFrame({'A': np.random.rand(3),
   .....:                     'B': 1,
   .....:                     'C': 'foo',
   .....:                     'D': pd.Timestamp('20010102'),
   .....:                     'E': pd.Series([1.0] * 3).astype('float32'),
   .....:                     'F': False,
   .....:                     'G': pd.Series([1] * 3, dtype='int8')})
   .....: 

In [329]: dft
Out[329]: 
          A  B    C          D    E      F  G
0  0.035962  1  foo 2001-01-02  1.0  False  1
1  0.701379  1  foo 2001-01-02  1.0  False  1
2  0.281885  1  foo 2001-01-02  1.0  False  1

In [330]: dft.dtypes
Out[330]: 
A           float64
B             int64
C            object
D    datetime64[ns]
E           float32
F              bool
G              int8
dtype: object

Seriesオブジェクトについては、dtype属性を用いる。

In [331]: dft['A'].dtype
Out[331]: dtype('float64')

pandasオブジェクトの一つの列の中に、複数のdtypeからなるデータがある場合 その列のdtypeは、全てのデータの型が格納できるような型が選ばれる(object型が最も汎用的である)。

# 6. が浮動小数点数なので、整数は浮動小数点数に変換される
In [332]: pd.Series([1, 2, 3, 4, 5, 6.])
Out[332]: 
0    1.0
1    2.0
2    3.0
3    4.0
4    5.0
5    6.0
dtype: float64

# 文字列のデータが含まれる場合は、強制的に"object" dtypeになる
In [333]: pd.Series([1, 2, 3, 6., 'foo'])
Out[333]: 
0      1
1      2
2      3
3      6
4    foo
dtype: object

DataFrame.dtypes.value_counts()を呼び出すと、DataFrameの中にそれぞれのデータ型の列がいくつあるかが分かる。

In [334]: dft.dtypes.value_counts()
Out[334]: 
bool              1
object            1
int8              1
float64           1
float32           1
int64             1
datetime64[ns]    1
dtype: int64

数値型のdtypeは、伝播し、DataFrameの中で共存できる。 (dtypeキーワード、渡されたndarray、渡されたSeriesのいずれかを通じて)dtypeが渡されたならば、 DataFrameの操作の中でdtypeは保存される。 さらに、異なる数値型は結合されない(訳注:ある列と別の列が独立なので、別の型で共存できる、ということを言っているのだろう) 以下の例を見れば、動作の一端が分かるだろう。

In [335]: df1 = pd.DataFrame(np.random.randn(8, 1), columns=['A'], dtype='float32')

In [336]: df1
Out[336]: 
          A
0  0.224364
1  1.890546
2  0.182879
3  0.787847
4 -0.188449
5  0.667715
6 -0.011736
7 -0.399073

In [337]: df1.dtypes
Out[337]: 
A    float32
dtype: object

In [338]: df2 = pd.DataFrame({'A': pd.Series(np.random.randn(8), dtype='float16'),
   .....:                     'B': pd.Series(np.random.randn(8)),
   .....:                     'C': pd.Series(np.array(np.random.randn(8),
   .....:                                             dtype='uint8'))})
   .....: 

In [339]: df2
Out[339]: 
          A         B    C
0  0.823242  0.256090    0
1  1.607422  1.426469    0
2 -0.333740 -0.416203  255
3 -0.063477  1.139976    0
4 -1.014648 -1.193477    0
5  0.678711  0.096706    0
6 -0.040863 -1.956850    1
7 -0.357422 -0.714337    0

In [340]: df2.dtypes
Out[340]: 
A    float16
B    float64
C      uint8
dtype: object

デフォルトの動作

デフォルトでは、整数の型はint64、浮動小数点数の型はfloat64であり、これはプラットフォーム(32ビットか64ビットか)に関係ない。以下のコードの結果は、全てint64 dtypeである。

In [341]: pd.DataFrame([1, 2], columns=['a']).dtypes
Out[341]: 
a    int64
dtype: object

In [342]: pd.DataFrame({'a': [1, 2]}).dtypes
Out[342]: 
a    int64
dtype: object

In [343]: pd.DataFrame({'a': 1}, index=list(range(2))).dtypes
Out[343]: 
a    int64
dtype: object

ただし、NumPyが配列を作るときには、型の選択はプラットフォームに依存するので注意。 以下のコードの結果は、32ビットのプラットフォームで実行した場合にはint32となる。

In [344]: frame = pd.DataFrame(np.array([1, 2]))

アップキャスト

型は、他の型と結合した場合にアップキャストされる可能性がある。すなわち、現在の型よりも上位の型に(例えばintからfloatに)変換される。

In [345]: df3 = df1.reindex_like(df2).fillna(value=0.0) + df2

In [346]: df3
Out[346]: 
          A         B      C
0  1.047606  0.256090    0.0
1  3.497968  1.426469    0.0
2 -0.150862 -0.416203  255.0
3  0.724370  1.139976    0.0
4 -1.203098 -1.193477    0.0
5  1.346426  0.096706    0.0
6 -0.052599 -1.956850    1.0
7 -0.756495 -0.714337    0.0

In [347]: df3.dtypes
Out[347]: 
A    float32
B    float64
C    float64
dtype: object

DataFrame.to_numpy() は、単一のdtypeのNumPy配列を返し、 その型はいわば「最小公倍数」、すなわち、元のDataFrameの全ての型を格納できるdtypeとなる。 これによりアップキャストが発生する場合がある。

In [348]: df3.to_numpy().dtype
Out[348]: dtype('float64')

astype

明示的に1つのdtypeから他のdtypeに変換するには、astypeメソッドを使うことができる。 astypeメソッドは*1デフォルトではコピーを返す。 これはdtypeが変わらない場合でも同じである。(動作を変更するには、copy=Falseパラメータを渡す。) さらに、astype操作が無効である場合、astypeメソッドは例外を発生させる。

アップキャストは常にNumPyの規則に従う。 ある演算に2つの異なるdtypeが関与している場合は、演算の結果には より汎用的なほうの型が使われる。

In [349]: df3
Out[349]: 
          A         B      C
0  1.047606  0.256090    0.0
1  3.497968  1.426469    0.0
2 -0.150862 -0.416203  255.0
3  0.724370  1.139976    0.0
4 -1.203098 -1.193477    0.0
5  1.346426  0.096706    0.0
6 -0.052599 -1.956850    1.0
7 -0.756495 -0.714337    0.0

In [350]: df3.dtypes
Out[350]: 
A    float32
B    float64
C    float64
dtype: object

# dtypesを変換する
In [351]: df3.astype('float32').dtypes
Out[351]: 
A    float32
B    float32
C    float32
dtype: object

astypeを使って、一部の列を特定の型に変更する。

In [352]: dft = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6], 'c': [7, 8, 9]})

In [353]: dft[['a', 'b']] = dft[['a', 'b']].astype(np.uint8)

In [354]: dft
Out[354]: 
   a  b  c
0  1  4  7
1  2  5  8
2  3  6  9

In [355]: dft.dtypes
Out[355]: 
a    uint8
b    uint8
c    int64
dtype: object
New in version 0.19.0.

astype()にdictを渡すことで、ある列(複数でもよい)を特定の型に変換する。

In [356]: dft1 = pd.DataFrame({'a': [1, 0, 1], 'b': [4, 5, 6], 'c': [7, 8, 9]})

In [357]: dft1 = dft1.astype({'a': np.bool, 'c': np.float64})

In [358]: dft1
Out[358]: 
       a  b    c
0   True  4  7.0
1  False  5  8.0
2   True  6  9.0

In [359]: dft1.dtypes
Out[359]: 
a       bool
b      int64
c    float64
dtype: object

注意: astype()およびloc()を用いて、列の部分集合を指定した型へと変換しようとした場合、アップキャストが発生する。 []はdtypeを括弧内で指定された型に変換するが*2、 その一方でloc()はオブジェクトを現在のdtypeに戻して代入しようとする。 ゆえに、下のコードの結果は意図した通りにならない。

In [360]: dft = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6], 'c': [7, 8, 9]})

In [361]: dft.loc[:, ['a', 'b']].astype(np.uint8).dtypes
Out[361]: 
a    uint8
b    uint8
dtype: object

In [362]: dft.loc[:, ['a', 'b']] = dft.loc[:, ['a', 'b']].astype(np.uint8)

In [363]: dft.dtypes
Out[363]: 
a    int64
b    int64
c    int64
dtype: object

オブジェクトの変換

pandasには、object dtypeから他の型に変換するための様々な関数が用意されている。 データが既に正しい型であるが、object型の配列に格納されている場合、 DataFrame.infer_objects() と Series.infer_objects()メソッドは正しい型にソフトに変換するのに使うことができる。(訳注:soft convertはこれ以上の説明が無い。「ユーザが型を指定せずに、データの中身から型を類推して変換してもらうこと」を指すか?)

In [364]: import datetime

In [365]: df = pd.DataFrame([[1, 2],
   .....:                    ['a', 'b'],
   .....:                    [datetime.datetime(2016, 3, 2),
   .....:                     datetime.datetime(2016, 3, 2)]])
   .....: 

In [366]: df = df.T

In [367]: df
Out[367]: 
   0  1          2
0  1  a 2016-03-02
1  2  b 2016-03-02

In [368]: df.dtypes
Out[368]: 
0            object
1            object
2    datetime64[ns]
dtype: object

データを転置したので、元々の型推測によって全ての列がobject型として格納されているが、infer_objectsを使うと修正される。 (訳注:「全ての列」と言いつつdatetime64[ns]型の列がある。謎。)

In [369]: df.infer_objects().dtypes
Out[369]: 
0             int64
1            object
2    datetime64[ns]
dtype: object

1次元の配列もしくはスカラーに対して、特定の型にハード変換するためには、以下の関数が使用できる。 (訳注:hard convertはこれ以上の説明が無い。「ユーザが『数値型にせよ』『日付型にせよ』などと型を指定して、データを変換すること」を指すか?)

to_numeric() (数値のdtypeへの変換)

In [370]: m = ['1.1', 2, 3]

In [371]: pd.to_numeric(m)
Out[371]: array([1.1, 2. , 3. ])

to_datetime() (datatime objectへの変換)

In [372]: import datetime

In [373]: m = ['2016-07-09', datetime.datetime(2016, 3, 2)]

In [374]: pd.to_datetime(m)
Out[374]: DatetimeIndex(['2016-07-09', '2016-03-02'], dtype='datetime64[ns]', freq=None)

to_timedelta() (timedelta objectへの変換)

In [375]: m = ['5us', pd.Timedelta('1day')]

In [376]: pd.to_timedelta(m)
Out[376]: TimedeltaIndex(['0 days 00:00:00.000005', '1 days 00:00:00'], dtype='timedelta64[ns]', freq=None)

型変換を強制する際には、errors引数を渡すことができる。(訳注:errors引数については「Pythonデータ分析/機械学習のための基本コーディング! pandasライブラリ活用入門」の第7章「データ型の概要と変換」にも同様の記述あり。) これにより、希望するdtypeやobjectに変換できなかった要素について、pandasがどう扱うかを指定する。 デフォルトではerrors='raise'であり、これは変換の過程の中で何らかのエラーに遭遇したらエラーが上がるという意味である。 しかし、もしerrors='coerce'を指定すれば、エラーは無視され、 pandasは問題を引き起こした要素をpd.NaT(datetime と timedeltaの場合)もしくはnp.nan(数値型の場合)に変換する。 この動作が便利かもしれないのは、読み込んだデータのほとんどが所望のdtype(例えば数値型やdatetime)であるが、 時々適合しない要素も混ざっていて、それらの要素を欠損として扱いたい場合である。

In [377]: import datetime

In [378]: m = ['apple', datetime.datetime(2016, 3, 2)]

In [379]: pd.to_datetime(m, errors='coerce')
Out[379]: DatetimeIndex(['NaT', '2016-03-02'], dtype='datetime64[ns]', freq=None)

In [380]: m = ['apple', 2, 3]

In [381]: pd.to_numeric(m, errors='coerce')
Out[381]: array([nan,  2.,  3.])

In [382]: m = ['apple', pd.Timedelta('1day')]

In [383]: pd.to_timedelta(m, errors='coerce')
Out[383]: TimedeltaIndex([NaT, '1 days'], dtype='timedelta64[ns]', freq=None)

errors引数には3番目の選択肢errors='ignore'があり、この場合、所望のデータ型に変換するときに何らかのエラーが発生したら、渡されたデータを変換せずにそのまま返す。 

In [384]: import datetime

In [385]: m = ['apple', datetime.datetime(2016, 3, 2)]

In [386]: pd.to_datetime(m, errors='ignore')
Out[386]: Index(['apple', 2016-03-02 00:00:00], dtype='object')

In [387]: m = ['apple', 2, 3]

In [388]: pd.to_numeric(m, errors='ignore')
Out[388]: array(['apple', 2, 3], dtype=object)

In [389]: m = ['apple', pd.Timedelta('1day')]

In [390]: pd.to_timedelta(m, errors='ignore')
Out[390]: array(['apple', Timedelta('1 days 00:00:00')], dtype=object)

オブジェクトの変換に加えて、to_numeric()にはもう一つの引数downcastがある。 引数downcastを使うと、新しく数値型になった(もしくは既に数値型の)データを、より小さいdtypeにダウンキャストできる。 これによってメモリが節約できる。

In [391]: m = ['1', 2, 3]

In [392]: pd.to_numeric(m, downcast='integer')   # smallest signed int dtype
Out[392]: array([1, 2, 3], dtype=int8)

In [393]: pd.to_numeric(m, downcast='signed')    # same as 'integer'
Out[393]: array([1, 2, 3], dtype=int8)

In [394]: pd.to_numeric(m, downcast='unsigned')  # smallest unsigned int dtype
Out[394]: array([1, 2, 3], dtype=uint8)

In [395]: pd.to_numeric(m, downcast='float')     # smallest float dtype
Out[395]: array([1., 2., 3.], dtype=float32)

上述したメソッドは、1次元の配列、リスト、スカラーにしか使えない。 DataFrameのような多次元のオブジェクトに直接使うことはできない。 しかし、apply()を使えば、効率的に各列に関数を適用(apply)できる。

In [396]: import datetime

In [397]: df = pd.DataFrame([
   .....:     ['2016-07-09', datetime.datetime(2016, 3, 2)]] * 2, dtype='O')
   .....: 

In [398]: df
Out[398]: 
            0                    1
0  2016-07-09  2016-03-02 00:00:00
1  2016-07-09  2016-03-02 00:00:00

In [399]: df.apply(pd.to_datetime)
Out[399]: 
           0          1
0 2016-07-09 2016-03-02
1 2016-07-09 2016-03-02

In [400]: df = pd.DataFrame([['1.1', 2, 3]] * 2, dtype='O')

In [401]: df
Out[401]: 
     0  1  2
0  1.1  2  3
1  1.1  2  3

In [402]: df.apply(pd.to_numeric)
Out[402]: 
     0  1  2
0  1.1  2  3
1  1.1  2  3

In [403]: df = pd.DataFrame([['5us', pd.Timedelta('1day')]] * 2, dtype='O')

In [404]: df
Out[404]: 
     0                1
0  5us  1 days 00:00:00
1  5us  1 days 00:00:00

In [405]: df.apply(pd.to_timedelta)
Out[405]: 
                0      1
0 00:00:00.000005 1 days
1 00:00:00.000005 1 days

落とし穴

(訳注:セクション名について*3

整数型のデータに対して要素の選択を実行すると、データが浮動小数点数にアップキャストされがちである *4。 結果にnanが含まれていない場合には、入力データのdtypeは保存される。 [整数のNAのサポート]も参照。

In [406]: dfi = df3.astype('int32')

In [407]: dfi['E'] = 1

In [408]: dfi
Out[408]: 
   A  B    C  E
0  1  0    0  1
1  3  1    0  1
2  0  0  255  1
3  0  1    0  1
4 -1 -1    0  1
5  1  0    0  1
6  0 -1    1  1
7  0  0    0  1

In [409]: dfi.dtypes
Out[409]: 
A    int32
B    int32
C    int32
E    int64
dtype: object

In [410]: casted = dfi[dfi > 0]

In [411]: casted
Out[411]: 
     A    B      C  E
0  1.0  NaN    NaN  1
1  3.0  1.0    NaN  1
2  NaN  NaN  255.0  1
3  NaN  1.0    NaN  1
4  NaN  NaN    NaN  1
5  1.0  NaN    NaN  1
6  NaN  NaN    1.0  1
7  NaN  NaN    NaN  1

In [412]: casted.dtypes
Out[412]: 
A    float64
B    float64
C    float64
E      int64
dtype: object

一方で、浮動小数点数のdtypeはnanが含まれても変更されない。

In [413]: dfa = df3.copy()

In [414]: dfa['A'] = dfa['A'].astype('float32')

In [415]: dfa.dtypes
Out[415]: 
A    float32
B    float64
C    float64
dtype: object

In [416]: casted = dfa[df2 > 0]

In [417]: casted
Out[417]: 
          A         B      C
0  1.047606  0.256090    NaN
1  3.497968  1.426469    NaN
2       NaN       NaN  255.0
3       NaN  1.139976    NaN
4       NaN       NaN    NaN
5  1.346426  0.096706    NaN
6       NaN       NaN    1.0
7       NaN       NaN    NaN

In [418]: casted.dtypes
Out[418]: 
A    float32
B    float64
C    float64
dtype: object

*1:この訳だと単数のmethodをTheseで受けていることになり変だ。自信がない

*2:ここの訳にぜんぜん自信がない。原文が間違っていないか?

*3:gotchaを「落とし穴」と訳した背景はこちらを参照。 linus-mk.hatenablog.com

*4:訳注:実のところ、このドキュメントを訳そうと思ったきっかけは、データが浮動小数点数に意図せずアップキャストされていたのが原因である。この記事を参照。 linus-mk.hatenablog.com