LoginSignup
1
2

PandasのSeriesに、なんと文字列の型が6種類もあるとは...

Last updated at Posted at 2024-03-09

update

  • 2024-03-17: 文字列操作の実践ケース(mapメソッドにdictを与えた変換)を追加
  • 2024-03-20: 文字列操作のベンチマークにいくつかのメソッドを追加

背景

Pandas は現在 major version が2系だが、2024年4月(以降)に 3.0のリリースが計画 されているようである(おそらく遅延するだろうと予測するが)。執筆時点での最新版は2.2.1であるが、2.2.0のリリースノートにて、pandas 3.0でのstring date typeの変更 についての記載を見つけたため、自分の調査した結果を、この記事でまとめることとした。

なお、pandas における pyarrow型の取り扱いはexperimentalであるため、2.2より将来で変更される可能性には留意が必要だ。

Prerequisites

import pandas # repr表示をそのまま実行できるようにするため
import pandas as pd
import pyarrow as pa
from pprint import pprint

pd.set_option('display.max_colwidth', 511)

print(pd.__version__)
# 2.2.1
print(pa.__version__)
# 15.0.1

今、何を使うべきかの個人的結論

  1. 互換性を大切にしたいか? Yes -> str. No -> 次へ
  2. experimentalを承知の上、ともかく内部メモリを削減したいか? Yes -> pd.ArrowDtype(pa.string()), No -> 次へ
  3. 特に深く考えず pd.NA を統一的に扱いたいか -> pd.StringDtype("python") (ie, "string" 指定、csv読み込み時は pd.read_csv(dtype_backend="numpy_nullable") で簡単指定)
  4. 何もせずしばらく様子見

コードでの挙動

まずは型指定とその実データで確認することにしてみる。何度も強調するが、v2.2.1 時点で experimental なため、将来の挙動について何も保証はない。

確認のためのデータ構成

  • pd.Series() には dtype= の指定が可能であるのでここで型を指定する
  • s0~s3は文字列でも指定が可能であるが、ここでは型クラスで指定する
  • missing value として None, pd.NA, float("nan") を指定してみる
s0 = pd.Series(["s0", None, pd.NA, float("nan")], name="s0", dtype=str) # dtype="str"
s1 = pd.Series(["s1", None, pd.NA, float("nan")], name="s1", dtype=pd.StringDtype("python")) # dtype="string[python]" or dtype="string"
s2 = pd.Series(["s2", None, pd.NA, float("nan")], name="s2", dtype=pd.StringDtype("pyarrow")) # dtype="string[pyarrow]"
s3 = pd.Series(["s3", None, pd.NA, float("nan")], name="s3", dtype=pd.StringDtype("pyarrow_numpy")) # dtype=string[pyarrow_numpy]"
s4 = pd.Series(["s4", None, pd.NA, float("nan")], name="s4", dtype=pd.ArrowDtype(pa.string()))
s5 = pd.Series(["s5", None, pd.NA, float("nan")], name="s5", dtype=pd.ArrowDtype(pa.large_string()))

なお、既存の文字型のSeriesは astype で相互変換可能である。

s0.astype(pd.ArrowDtype(pa.large_string())).dtype
# large_string[pyarrow]

データの確認

s0~s5 の dtype や dtypeのtypeを確認してみる。

ss = [s0, s1, s2, s3, s4, s5]

各Seriesの要素について

pd.concat(ss, axis=1)
#      s0    s1    s2   s3    s4    s5
# 0    s0    s1    s2   s3    s4    s5
# 1  None  <NA>  <NA>  NaN  <NA>  <NA>
# 2  <NA>  <NA>  <NA>  NaN  <NA>  <NA>
# 3   NaN  <NA>  <NA>  NaN  <NA>  <NA>
各要素のtype
pprint({s.name: s.map(type).tolist() for s in ss})
{'s0': [<class 'str'>,
        <class 'NoneType'>,
        <class 'pandas._libs.missing.NAType'>,
        <class 'float'>],
 's1': [<class 'str'>,
        <class 'pandas._libs.missing.NAType'>,
        <class 'pandas._libs.missing.NAType'>,
        <class 'pandas._libs.missing.NAType'>],
 's2': [<class 'str'>,
        <class 'pandas._libs.missing.NAType'>,
        <class 'pandas._libs.missing.NAType'>,
        <class 'pandas._libs.missing.NAType'>],
 's3': [<class 'str'>, <class 'float'>, <class 'float'>, <class 'float'>],
 's4': [<class 'str'>,
        <class 'pandas._libs.missing.NAType'>,
        <class 'pandas._libs.missing.NAType'>,
        <class 'pandas._libs.missing.NAType'>],
 's5': [<class 'str'>,
        <class 'pandas._libs.missing.NAType'>,
        <class 'pandas._libs.missing.NAType'>,
        <class 'pandas._libs.missing.NAType'>]}
  • s0: オブジェクト型なので入力値通り
  • s3: missing value は全て float 型(すなわち float("nan"))として扱われる
  • s1,s2,s4,s5: missing value は全て pd.NA として扱われる

dtypeについて

 pprint({s.name: s.dtypes for s in ss})
{'s0': dtype('O'),
 's1': string[python],
 's2': string[pyarrow],
 's3': string[pyarrow_numpy],
 's4': string[pyarrow],
 's5': large_string[pyarrow]}
  • s0は伝統的な dtype('O') のオブジェクト型である
  • S1~S4は string[...] のように型パラメータが表示されている
  • s2とs4の表示は string[pyarrow] 同一だが、次述のtype(s.dtype) は異なるため、dtype指定と一対一ではない(要注意

s3の string[pyarrow_numpy] について、公式ドキュメント 2.2.0 Release 時の Upcoming Change in Pandas 3.0 では、3.0でデフォルトになるとの記述がある(本当にそうするのか?)。2.2.1でも下記のように pd.options.future.infer_string = True を設定すると、挙動を変更できる。

with pd.option_context("future.infer_string", True):
    ser = pd.Series(["a", "b"])
    repr(ser.dtype)
    # 'string[pyarrow_numpy]'
ser = pd.Series(["a", "b"])
repr(ser.dtype)
# "dtype('O')"

個人的な感想だが、この調査をするまでこの型の存在すら知らなかったし、後述の挙動の比較からもデフォルトとして扱うには他よりそこまで利点を感じない。

dtype の type について

pprint({s.name: type(s.dtypes) for s in ss})
{'s0': <class 'numpy.dtypes.ObjectDType'>,
 's1': <class 'pandas.core.arrays.string_.StringDtype'>,
 's2': <class 'pandas.core.arrays.string_.StringDtype'>,
 's3': <class 'pandas.core.arrays.string_.StringDtype'>,
 's4': <class 'pandas.core.dtypes.dtypes.ArrowDtype'>,
 's5': <class 'pandas.core.dtypes.dtypes.ArrowDtype'>}
  • s0は素のnumpy dtypeのオブジェクト型
  • s1~s3は pandas.core.arrays.string_.StringDtype(いわゆる Extension Array)
  • s4~s5は pandas.core.dtypes.dtypes.ArrowDtype (おそらく素の arrowベース)

dtype の array type について

pprint({s.name:  s.dtypes.construct_array_type() if hasattr(s.dtypes, "construct_array_type") else "---" for s in ss})
{'s0': '---',
 's1': <class 'pandas.core.arrays.string_.StringArray'>,
 's2': <class 'pandas.core.arrays.string_arrow.ArrowStringArray'>,
 's3': <class 'pandas.core.arrays.string_arrow.ArrowStringArrayNumpySemantics'>,
 's4': <class 'pandas.core.arrays.arrow.array.ArrowExtensionArray'>,
 's5': <class 'pandas.core.arrays.arrow.array.ArrowExtensionArray'>}
  • s0は存在しない(素のnumpy dtypeなので)
  • s1,s2,s3の string[...]の array type の実態はそれぞれ異なる
  • s4,s5は同一で、array typeは arrowベースの ArrowExtensionArray

もっと簡単な確認方法

Series.array の repr 表示が一番楽かもしれない

[repr(s.array) for s in ss]

["<NumpyExtensionArray>\n['s0', None, <NA>, nan]\nLength: 4, dtype: object",
 "<StringArray>\n['s1', <NA>, <NA>, <NA>]\nLength: 4, dtype: string",
 "<ArrowStringArray>\n['s2', <NA>, <NA>, <NA>]\nLength: 4, dtype: string",
 "<ArrowStringArrayNumpySemantics>\n['s3', nan, nan, nan]\nLength: 4, dtype: string",
 "<ArrowExtensionArray>\n['s4', <NA>, <NA>, <NA>]\nLength: 4, dtype: string[pyarrow]",
 "<ArrowExtensionArray>\n['s5', <NA>, <NA>, <NA>]\nLength: 4, dtype: large_string[pyarrow]"]

型クラスそのものからの情報

実は上記 s1~s5で指定した型クラスそのもの method, property から各種情報を確認することがきる。

string_types = [
  pd.StringDtype("python"), 
  pd.StringDtype("pyarrow"), 
  pd.StringDtype("pyarrow_numpy"), 
  pd.ArrowDtype(pa.string()), 
  pd.ArrowDtype(pa.large_string())
]
pprint([{
    "variable": f"s{i}",
    "name": d.name,
    "storage": d.storage,
    "na_value": str(d.na_value),
    "type": str(d.type),
    "construct_array_type": str(d.construct_array_type()),
    "_metadata": str(d._metadata),
    } 
    for i, d in enumerate(string_types, 1)],
sort_dicts=False, width=120)

[{'variable': 's1',
  'name': 'string',
  'storage': 'python',
  'na_value': '<NA>',
  'type': "<class 'str'>",
  'construct_array_type': "<class 'pandas.core.arrays.string_.StringArray'>",
  '_metadata': "('storage',)"},
 {'variable': 's2',
  'name': 'string',
  'storage': 'pyarrow',
  'na_value': '<NA>',
  'type': "<class 'str'>",
  'construct_array_type': "<class 'pandas.core.arrays.string_arrow.ArrowStringArray'>",
  '_metadata': "('storage',)"},
 {'variable': 's3',
  'name': 'string',
  'storage': 'pyarrow_numpy',
  'na_value': 'nan',
  'type': "<class 'str'>",
  'construct_array_type': "<class 'pandas.core.arrays.string_arrow.ArrowStringArrayNumpySemantics'>",
  '_metadata': "('storage',)"},
 {'variable': 's4',
  'name': 'string[pyarrow]',
  'storage': 'pyarrow',
  'na_value': '<NA>',
  'type': "<class 'str'>",
  'construct_array_type': "<class 'pandas.core.arrays.arrow.array.ArrowExtensionArray'>",
  '_metadata': "('storage', 'pyarrow_dtype')"},
 {'variable': 's5',
  'name': 'large_string[pyarrow]',
  'storage': 'pyarrow',
  'na_value': '<NA>',
  'type': "<class 'str'>",
  'construct_array_type': "<class 'pandas.core.arrays.arrow.array.ArrowExtensionArray'>",
  '_metadata': "('storage', 'pyarrow_dtype')"}]

コンストラクタからの直接の作成

以下のように、構成されているデータが np.arraypa.array のどちらかが明瞭である。

pd.arrays.NumpyExtensionArray(np.array(["s0",None,pd.NA,float("nan")], dtype="object")) # s0.array

pd.arrays.StringArray(np.array(["s1",None,None,None])) # s1.array

pd.arrays.ArrowStringArray(pa.array(["s2",None,None,None])) # s2.array

# pd.arrays には公開されていない
pd.core.arrays.string_arrow.ArrowStringArrayNumpySemantics(pa.array(["s3",None,None,None])) # s3

pd.arrays.ArrowExtensionArray(pa.array(["s4",None,None,None])) # s4.array

pd.arrays.ArrowExtensionArray(pa.array(["s5",None,None,None], type="large_string")) # s5.array

文字列操作後の型について

str -> bool の真偽値変換

s0.str.contains("x").dtypes,\
s1.str.contains("x").dtypes,\
s2.str.contains("x").dtypes,\
s3.str.contains("x").dtypes,\
s4.str.contains("x").dtypes,\
s5.str.contains("x").dtypes

# (dtype('O'), BooleanDtype, BooleanDtype, dtype('O'), bool[pyarrow], bool[pyarrow]) 
  • s0, s3 は 伝統的なObject型となる。s3は NaN を含んでいるからであるが、NaNを含まない場合は bool型 となるので注意
    >>> s0.str.contains("x")
    0    False
    1     None
    2     <NA>
    3      NaN
    Name: s0, dtype: object
    >>> s3.str.contains("x")
    0    False
    1      NaN
    2      NaN
    3      NaN
    Name: s3, dtype: object
    
    >>> s3.fillna("x").dtypes
    string[pyarrow_numpy]
    >>> s3.fillna("x").str.contains("x")
    0    False
    1     True
    2     True
    3     True
    Name: s3, dtype: bool
    
  • s1, s2は boolean (pd.NAを含むことができるExtension Type)となる
  • s4, s5は bool[pyarrow] となる

ここで、 s3 の挙動について nan の有無により型が変わりうる問題について issue#54805 でも報告されている。

str -> int の変換について

s0.str.len().dtypes,\
s1.str.len().dtypes,\
s2.str.len().dtypes,\
s3.str.len().dtypes,\
s4.str.len().dtypes,\
s5.str.len().dtypes

# (dtype('O'), Int64Dtype(), Int64Dtype(), dtype('float64'), int32[pyarrow], int64[pyarrow])

s0.fillna("x").str.len().dtypes,\
s1.fillna("x").str.len().dtypes,\
s2.fillna("x").str.len().dtypes,\
s3.fillna("x").str.len().dtypes,\
s4.fillna("x").str.len().dtypes,\
s5.fillna("x").str.len().dtypes

# (dtype('int64'), Int64Dtype(), Int64Dtype(), dtype('int64'), int32[pyarrow], int64[pyarrow])

真偽値変換と同様の傾向である。すなわち:

  • s0, s3 は 伝統的な型(単純なnumpyベース)となる。s3は NaN を含んでいるためint系ではなく float64 となっている、NaNを含まない場合は int64 となることに注意
  • s1, s2は Int64型 (pd.NAを含むことができるExtension Type)となる
  • s4は int32[pyarrow]、s5は int64[pyarrow] となる。large_string 2GB以上の文字を扱う型なのでは サイズint64 を返す型となるのは整合的であろう

もう少し実践的なケース

male, female0, 1 の 数値に変換。適度に missing value を含んだものとする。

import random
random.seed(15)
s = random.choices(["male","female",None],weights=[49,49,2],k=1_000_000)

ss0 = pd.Series(s, dtype="str")
ss1 = pd.Series(s, dtype=pd.StringDtype("python"))
ss2 = pd.Series(s, dtype=pd.StringDtype("pyarrow"))
ss3 = pd.Series(s, dtype=pd.StringDtype("pyarrow_numpy")) 
ss4 = pd.Series(s, dtype=pd.ArrowDtype(pa.string()))
ss5 = pd.Series(s, dtype=pd.ArrowDtype(pa.large_string()))

ケース1

pd.Series.map で dictionaryを引数に与える場合、全ての結果は nan を含んだfloat64となる。

[s.map({"male":0, "female":1}).head() for s in [ss0, ss1, ss2, ss3, ss4, ss5]]
[0    1.0
 1    0.0
 2    1.0
 3    0.0
 4    NaN
 dtype: float64,
 0    1.0
 1    0.0
 2    1.0
 3    0.0
 4    NaN
 dtype: float64,
 0    1.0
 1    0.0
 2    1.0
 3    0.0
 4    NaN
 dtype: float64,
 0    1.0
 1    0.0
 2    1.0
 3    0.0
 4    NaN
 dtype: float64,
 0    1.0
 1    0.0
 2    1.0
 3    0.0
 4    NaN
 dtype: float64,
 0    1.0
 1    0.0
 2    1.0
 3    0.0
 4    NaN
 dtype: float64]

このケース1での実行速度は以下の通り(ipython magic)。s0,s1が10倍程度早い。s2~s5のarrow系データの操作全般遅いのは少し予想外だった。

In [290]: %timeit ss0.map({"male":0, "female":1})
18.4 ms ± 472 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [291]: %timeit ss1.map({"male":0, "female":1})
12 ms ± 191 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [292]: %timeit ss2.map({"male":0, "female":1})
114 ms ± 3.65 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [293]: %timeit ss3.map({"male":0, "female":1})
120 ms ± 2.22 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [294]: %timeit ss4.map({"male":0, "female":1})
128 ms ± 4.44 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [295]: %timeit ss5.map({"male":0, "female":1})
124 ms ± 3.28 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

なお、mapの引数のdictのkeyに含まれない値は nan となることに注意。s0~s5全て共通。

ss0.map({"male":0}).head()
0    NaN
1    0.0
2    NaN
3    0.0
4    NaN
dtype: float64

defaultdictを与えれば、keyに含まれない値の変換後の値を指定ができる。s0~s5全て共通。

from collections import defaultdict
ss0.map(defaultdict(lambda: -1, {"male": 0})).head()
0   -1
1    0
2   -1
3    0
4   -1
dtype: int64

ケース2

pd.Series.mask について、s1~25はそもそも 文字列 -> 数値 の型変換を伴う場合はエラーとなる。文字列- > 文字列|pd.NA のみ受け付けるようだ。
なお Pandas 2.2.0 から導入された case_when メソッドも、内部では mask が使われているのでエラーとなる。

pd.Series(["male", "male"], dtype="string[python]").mask(lambda s: s=="male", 0)
ValueError: StringArray requires a sequence of strings or pandas.NA

mask実行後の型がmixするケースも(そもそも 文字列 -> 数値 の変換の時点で)、s1~s5は当然エラーとなる。

ss0.mask(lambda s: s=="male", 0).head()
# Out[230]:
# 0    female
# 1         0
# 2    female
# 3         0
# 4      None
# dtype: object

ss1.mask(lambda s: s=="male", 0).head()
# ValueError: StringArray requires a sequence of strings or pandas.NA

ss2.mask(lambda s: s=="male", 0).head()
# TypeError: Scalar must be NA or str

ss3.mask(lambda s: s=="male", 0).head()
# TypeError: Scalar must be NA or str

ss4.mask(lambda s: s=="male", 0).head()
# TypeError: Invalid value '0' for dtype string[pyarrow]

ss5.mask(lambda s: s=="male", 0).head()
# TypeError: Invalid value '0' for dtype large_string[pyarrow]

メモリ使用量について

結論:

  • str, pd.StringDtype("python") の使用量は同量で一番多い
  • その他は上記より少ない。おおよそ str の25%~50%程度
  • その中でもさらに pd.ArrowDtype(pa.string()) は 10~20%少なくなりうる

100万行、要素は1文字

要素が1文字は6~10倍程度効率的が異なる。ただし人工データとしての参考値でしかない。

df0 = (
    pd.DataFrame({
        "s0": ["x"] * n, 
        "s1": ["x"] * n, 
        "s2": ["x"] * n, 
        "s3": ["x"] * n, 
        "s4": ["x"] * n,
        "s5": ["x"] * n,
    })
    .astype({
        "s0": str,
        "s1": pd.StringDtype("python"),
        "s2": pd.StringDtype("pyarrow"), 
        "s3": pd.StringDtype("pyarrow_numpy"), 
        "s4": pd.ArrowDtype(pa.string()),
        "s5": pd.ArrowDtype(pa.large_string()),
    })
)

df0.memory_usage(deep=True)
# Index         132
# s0       58000000
# s1       58000000
# s2        9000000
# s3        9000000
# s4        5000000
# s5        9000000
# dtype: int64
  • s0, s1 が58MBと突出
  • s2, s3, s5 が 9MB
  • s4 が5MB

100万行、要素長は36文字(ハイフン込みuuid文字列)

uuid文字列はシナリオとしてありうる。2倍ほど効率的


import uuid

def uuids(n: int) -> list[str]:
    return [str(uuid.uuid4()) for _ in range(n)]

n = 1_000_000
df1 = (
    pd.DataFrame({
        "s0": uuids(n), 
        "s1": uuids(n), 
        "s2": uuids(n), 
        "s3": uuids(n), 
        "s4": uuids(n),
        "s5": uuids(n),
    })
    .astype({
        "s0": str,
        "s1": pd.StringDtype("python"),
        "s2": pd.StringDtype("pyarrow"), 
        "s3": pd.StringDtype("pyarrow_numpy"), 
        "s4": pd.ArrowDtype(pa.string()),
        "s5": pd.ArrowDtype(pa.large_string()),
    })
)

df1.memory_usage(deep=True)
# Index         132
# s0       93000000
# s1       93000000
# s2       44000000
# s3       44000000
# s4       40000000
# s5       44000000
  • s0, s1 が93MB
  • s2, s3, s5が40MB
  • s4 が40MB

100万行、1~15文字ランダム

人工データだがありうるケース。4.0~5.4倍ほど効率

import random, string
def random_chars(n: int, k: int, seed: int = 1) -> list[str]:
    random.seed(seed)
    candi = string.ascii_letters + string.ascii_uppercase
    r = range(1, k)
    def k_word() -> str:
        return "".join(random.choices(candi, k=random.choice(r))) 
    return [k_word() for n in range(n)]

n = 1_000_000
k = 16
df2 = (
    pd.DataFrame({
        "a": random_chars(n, k), 
        "b": random_chars(n, k), 
        "c": random_chars(n, k), 
        "d": random_chars(n, k), 
        "e": random_chars(n, k),
        "f": random_chars(n, k),
    })
    .astype({
        "a": str,
        "b": pd.StringDtype("python"),
        "c": pd.StringDtype("pyarrow"), 
        "d": pd.StringDtype("pyarrow_numpy"), 
        "e": pd.ArrowDtype(pa.string()),
        "f": pd.ArrowDtype(pa.large_string()),
    })
)
df2.memory_usage(deep=True)
# Index         132
# s0       65001139
# s1       65001139
# s2       16001139
# s3       16001139
# s4       12001139
# s5       16001139
# dtype: int64
  • s0, s1 が65MB
  • s2, s3, s5が16MB
  • s4 が12MB

文字操作の実行速度について

実行速度について、内部的にarrowを使うs2, s3, s4, s5が 5(~20)倍 速くなる。データが多量かつstr method 操作を多用する場合はトータルの実行時間でも体感的に差を感じるであろう。ただし、前述のmapメソッドでは逆にarrow系が10倍も遅くなるなど、strメソッド以外では必ずしも高速にはならないことに注意が必要である。

str.len()
In [9]: %timeit df2.a.str.len()
139 ms ± 1.64 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [10]: %timeit df2.b.str.len()
41.1 ms ± 1.67 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [11]: %timeit df2.c.str.len()
11.4 ms ± 336 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [12]: %timeit df2.d.str.len()
9.84 ms ± 138 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [13]: %timeit df2.e.str.len()
9.81 ms ± 168 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [14]: %timeit df2.f.str.len()
9.98 ms ± 141 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
str.startswith()
In [15]: %timeit df2.a.str.startswith("xyz")
138 ms ± 1.96 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [16]: %timeit df2.b.str.startswith("xyz")
124 ms ± 1.03 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [17]: %timeit df2.c.str.startswith("xyz")
6.93 ms ± 132 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [18]: %timeit df2.d.str.startswith("xyz")
6.89 ms ± 108 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [19]: %timeit df2.e.str.startswith("xyz")
5.79 ms ± 97.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [20]: %timeit df2.f.str.startswith("xyz")
6.04 ms ± 92 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
str.slice
In [305]:  %timeit df2.a.str[:4]
155 ms ± 15 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [306]:  %timeit df2.b.str[:4]
188 ms ± 8.81 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [307]:  %timeit df2.c.str[:4]
19.8 ms ± 491 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [308]:  %timeit df2.d.str[:4]
20.7 ms ± 493 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [309]:  %timeit df2.e.str[:4]
20.5 ms ± 768 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [310]:  %timeit df2.f.str[:4]
21.2 ms ± 712 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
str.replace()
In [311]:  %timeit df2.a.str.replace("xyz", "abc")
156 ms ± 1.47 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [312]:  %timeit df2.b.str.replace("xyz", "abc")
168 ms ± 7.38 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [313]:  %timeit df2.c.str.replace("xyz", "abc")
33.6 ms ± 1.06 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [314]:  %timeit df2.d.str.replace("xyz", "abc")
31.6 ms ± 1.37 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [315]:  %timeit df2.e.str.replace("xyz", "abc")
31.5 ms ± 1.06 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [316]:  %timeit df2.f.str.replace("xyz", "abc")
31.4 ms ± 1.79 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
str.lower()
In [324]: %timeit df2.a.str.lower()
106 ms ± 2.92 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [325]: %timeit df2.b.str.lower()
113 ms ± 4.15 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [326]: %timeit df2.c.str.lower()
21.4 ms ± 609 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [327]: %timeit df2.d.str.lower()
22.5 ms ± 604 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [328]: %timeit df2.e.str.lower()
21.6 ms ± 1.12 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [329]: %timeit df2.f.str.lower()
22.8 ms ± 1.02 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

pd.read_csv()の挙動

このサンプルデータを読み込む。

from io import StringIO
s = """col0,col1
0,row1
1,None
2,
3,NA
4,NaN
"""

pd.read_csv(dtype=...)の指定

pd.read_csv() での dtype の指定も問題ない。strでの missing value については全て nan となることに注意。

pprint(
  [pd.read_csv(StringIO(s), dtype={"col1": t}).col1.array for t in [str] + string_types]
)

[<NumpyExtensionArray>
['row1', nan, nan, nan, nan]
Length: 5, dtype: object,
 <StringArray>
['row1', <NA>, <NA>, <NA>, <NA>]
Length: 5, dtype: string,
 <ArrowStringArray>
['row1', <NA>, <NA>, <NA>, <NA>]
Length: 5, dtype: string,
 <ArrowStringArrayNumpySemantics>
['row1', nan, nan, nan, nan]
Length: 5, dtype: string,
 <ArrowExtensionArray>
['row1', <NA>, <NA>, <NA>, <NA>]
Length: 5, dtype: string[pyarrow],
 <ArrowExtensionArray>
['row1', <NA>, <NA>, <NA>, <NA>]
Length: 5, dtype: large_string[pyarrow]]

pd.read_csv(dtype_backend=...)の指定

以下、s1 として読み込まれる。

pd.read_csv(StringIO(s), dtype_backend="numpy_nullable").col1.array

<StringArray>
['row1', <NA>, <NA>, <NA>, <NA>]
Length: 5, dtype: string

以下、s4として読み込まれる。

pd.read_csv(StringIO(s), dtype_backend="pyarrow").col1.array

<ArrowExtensionArray>
['row1', <NA>, <NA>, <NA>, <NA>]
Length: 5, dtype: string[pyarrow]

なお、 pd.read_csv には engine="pyarrow" というengine指定もあるが、これは csv の文字の読み込み時のparseを何で行うかの指定であり、読み込んだ後の型には(理想的には)影響しないものである。(parse_datesの指定では一部想定外のケースあり)

その他

な、なんと numpy 2.0 が2024年春以降に公開予定だが、可変長UTF8の文字列型が StringDType として新規導入予定のようだ。pandas 3.0でどのように連携されるかは、まったくの調査不足のため不明である。混沌としてきて、もはや Polars に移行してしまえば全く悩みはなくなるのでは、と個人的には思うばかりである。Pandasなんもわからん。

References

https://pandas.pydata.org/docs/user_guide/text.html
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.StringDtype.html
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.ArrowDtype.html

1
2
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
1
2