Date

TL;DR

  • pandasではmissin valueを含むboolean型はobject型になってしまう
  • pandas.assignやpandas.joinなどでいつの間にかobject型になっているので注意!
  • object型をbool型にキャストすると、bool(np.nan)==True なので注意!

bool(np.nan)==True

np.nanとの比較は常にFalseとなる.

In [1]:
False == np.nan
Out[1]:
False
In [2]:
True == np.nan
Out[2]:
False

しかし(?)np.nanをboolにキャストするとTrueになる

In [3]:
bool(np.nan)
Out[3]:
True

一方intへのキャストはエラーとなる

In [4]:
int(np.nan)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-4-03e21f51ddd3> in <module>()
----> 1 int(np.nan)

ValueError: cannot convert float NaN to integer

np.nanの比較はややこしいので、np.isnanやpd.isnaをつかおう

In [5]:
assert (np.nan == np.nan) == False
assert (np.nan is np.nan) == True
assert (np.nan == float('nan')) == False
assert (float('nan') is float('nan')) == False

まとめると、

In [6]:
s = [True,False,np.nan,1,0,'1','0']
df = pd.DataFrame().assign(tfn=s, typ=[type(a) for a in s])
df.assign(**{
    'astype(bool)' : df.tfn.astype(bool),
    'astype(float)' : df.tfn.astype(float),
    '==True' : df.tfn==True,
    '==False' : df.tfn==False,
    '==1' : df.tfn==1,
    '==0' : df.tfn==0,
    'is np.nan' : df.tfn is np.nan,
    'isna' : pd.isna(df.tfn),
})
Out[6]:
tfn typ astype(bool) astype(float) ==True ==False ==1 ==0 is np.nan isna
0 True <class 'bool'> True 1.0 True False True False False False
1 False <class 'bool'> False 0.0 False True False True False False
2 NaN <class 'float'> True NaN False False False False False True
3 1 <class 'int'> True 1.0 True False True False False False
4 0 <class 'int'> False 0.0 False True False True False False
5 1 <class 'str'> True 1.0 False False False False False False
6 0 <class 'str'> True 0.0 False False False False False False

cf

np.nanの実態は、floating pointのNaNで、

この辺で定義されている.

In [8]:
import struct
hex(struct.unpack('<I', struct.pack('<f', np.nan))[0])
Out[8]:
'0x7fc00000'
In [9]:
struct.unpack('!f', bytes.fromhex('7fc00000'))[0]==np.nan
Out[9]:
False

この辺の挙動がよくわからんが、とにかくnp.isnanやpd.isnaを使おう

pandasでは、missing valueがあるとboolはobjectにupcastされる

pandas DataFrameにおけるbooleanの計算は基本的にnumpyと同じ動きになっている。 boolean型はTrueもしくはFalseであり、NaN (np.nan) をとることができない。 そのため、pandas は missing valueを含むboolean型は、object型にupcastして管理する。

例として、pandas.assignの場合

In [10]:
series = pd.Series([True,False], index=[1,2])
display(series)

df = pd.DataFrame().assign(iroha=list('イロハニ'))
display(df)
1     True
2    False
dtype: bool
iroha
0
1
2
3

ここで、seriesをdfにassignすると、missing valueに対して np.nan が割り振られる。bool型はnp.nanを保持できないため、object型にアップキャストされる

In [11]:
dfa = df.assign(b = series)
dfa
Out[11]:
iroha b
0 NaN
1 True
2 False
3 NaN
In [12]:
dfa.b.dtype
Out[12]:
dtype('O')

assignの他に、merge/joinなどでも同様である

それを防ぐには, join後に fillna() してから astype(bool)

(ただしmissing valueの情報を保持しい場合は、floatに変換したほうがいい場合もある.)

In [13]:
def join_bool(left_db, right_db, fillna=False):
    def get_bool_columns(db):
        return [c for c in db.columns if db[c].dtype.name=='bool']

    bool_cols = set(get_bool_columns(left_db) + get_bool_columns(right_db))

    ret = left_db.join(right_db, how='outer')

    for col in bool_cols:
        ret[col] = ret[col].fillna(fillna).astype(bool)
    
    return ret

join_bool(df, series.to_frame())
Out[13]:
iroha 0
0 False
1 True
2 False
3 False

pandasにおけるbool型の扱い

pandasではbool型に対する計算は、True=1.0, False=0.0 の様に扱ってくれるので、sumやmeanが直感的で便利

In [14]:
pd.Series([np.nan, True, False], dtype='object').sum()
Out[14]:
1
In [15]:
pd.Series([np.nan, True, False], dtype='object').mean()
Out[15]:
0.5
In [16]:
pd.Series([np.nan], dtype='object').mean()
Out[16]:
nan

ただ、こういうことがしたいのであれば、boolをfloatに変換するほうがよい

object型のboolはすぐに泥沼にはまる

In [17]:
pd.Series([np.nan], dtype='object').sum()
Out[17]:
0
In [18]:
pd.Series([True],dtype='object').sum()
Out[18]:
True
In [19]:
pd.Series([True, np.nan],dtype='object').sum()
Out[19]:
1
In [20]:
pd.Series([False],dtype='object').sum()
Out[20]:
False
In [21]:
pd.Series([False, np.nan],dtype='object').sum()
Out[21]:
0

結果として、groupby-sum とかが大体こける。rowsが大きいとすごく時間もかかる

In [22]:
df = pd.DataFrame().assign(name=list('ABBCCCD'))
df = df.assign(boolean = pd.Series([True,True,False,True,False]))
df
Out[22]:
name boolean
0 A True
1 B True
2 B False
3 C True
4 C False
5 C NaN
6 D NaN
In [23]:
df.groupby('name').sum()
Out[23]:
boolean
name
A True
B 1
C 1
D 0

object型のboolは大抵利点がほとんど無い。かといってすべてfillna().astype(bool)しても欠損値の情報が抜ける.

まとめ

missing valueを含む真偽値は、カラムの特性に応じて適宜変換してから使う

  • missing valueを考慮しながらTrueの割合など計算する目的であれば、floatへ変換する => x.astype(float)
  • Trueのものに意味があるのであれば、=> x.fillna(False).astype(bool)
  • x.astype(bool) は、x.fillna(True).astype(bool) の意味になる

ちなみに次回pandasには、NaNを含むintがサポートされるようだ

http://pandas-docs.github.io/pandas-docs-travis/whatsnew.html#optional-integer-na-support


Comments

comments powered by Disqus