dtype konvertieren

Manchmal passen die pandas-Datentypen nicht wirklich gut. Dies kann z.B. auf Serialisierungsformate zurückzuführen sein, die keine Typinformationen enthalten. Manchmal solltet ihr jedoch den Typ auch ändern, um eine bessere Performance zu erzielen – entweder mehr Manipulationsmöglichkeiten oder weniger Speicherbedarf. In den folgenden Beispielen werden wir verschiedene Konvertierungen einer Series vornehmen:

[1]:
import numpy as np
import pandas as pd
[2]:
rng = np.random.default_rng()
s = pd.Series(rng.normal(size=7))
[3]:
s
[3]:
0   -1.663472
1    0.205134
2    0.333356
3   -0.639533
4    1.519715
5    0.331444
6    1.551766
dtype: float64

Automatische Konvertierung

pandas.Series.convert_dtypes versucht, eine Series in einen Typ zu konvertieren, der NA unterstützt. Im Fall unserer Series wird der Typ von float64 in Float64 geändert:

[4]:
s.convert_dtypes()
[4]:
0   -1.663472
1    0.205134
2    0.333356
3   -0.639533
4    1.519715
5    0.331444
6    1.551766
dtype: Float64

Bedauerlicherweise habe ich jedoch mit convert_dtypes kaum Kontrolle darüber, in welchen Datentyp konvertiert wird. Daher bevorzuge ich pandas.Series.astype:

[5]:
s.astype("float32")
[5]:
0   -1.663472
1    0.205134
2    0.333356
3   -0.639533
4    1.519715
5    0.331444
6    1.551766
dtype: float32

Sofern jedoch nicht konvertierbare Werte enthalten sind, wird ein Fehler ausgegeben:

[6]:
rng = np.random.default_rng()
n = pd.Series([rng.integers(127), np.nan, rng.integers(127)])
n
[6]:
0    90.0
1     NaN
2     1.0
dtype: float64
[7]:
n.astype("int8")
---------------------------------------------------------------------------
IntCastingNaNError                        Traceback (most recent call last)
Cell In[7], line 1
----> 1 n.astype("int8")

File ~/cusy/trn/jupyter-tutorial/uvenvs/py313/.venv/lib/python3.13/site-packages/pandas/core/generic.py:6643, in NDFrame.astype(self, dtype, copy, errors)
   6637     results = [
   6638         ser.astype(dtype, copy=copy, errors=errors) for _, ser in self.items()
   6639     ]
   6641 else:
   6642     # else, only a single dtype is given
-> 6643     new_data = self._mgr.astype(dtype=dtype, copy=copy, errors=errors)
   6644     res = self._constructor_from_mgr(new_data, axes=new_data.axes)
   6645     return res.__finalize__(self, method="astype")

File ~/cusy/trn/jupyter-tutorial/uvenvs/py313/.venv/lib/python3.13/site-packages/pandas/core/internals/managers.py:430, in BaseBlockManager.astype(self, dtype, copy, errors)
    427 elif using_copy_on_write():
    428     copy = False
--> 430 return self.apply(
    431     "astype",
    432     dtype=dtype,
    433     copy=copy,
    434     errors=errors,
    435     using_cow=using_copy_on_write(),
    436 )

File ~/cusy/trn/jupyter-tutorial/uvenvs/py313/.venv/lib/python3.13/site-packages/pandas/core/internals/managers.py:363, in BaseBlockManager.apply(self, f, align_keys, **kwargs)
    361         applied = b.apply(f, **kwargs)
    362     else:
--> 363         applied = getattr(b, f)(**kwargs)
    364     result_blocks = extend_blocks(applied, result_blocks)
    366 out = type(self).from_blocks(result_blocks, self.axes)

File ~/cusy/trn/jupyter-tutorial/uvenvs/py313/.venv/lib/python3.13/site-packages/pandas/core/internals/blocks.py:758, in Block.astype(self, dtype, copy, errors, using_cow, squeeze)
    755         raise ValueError("Can not squeeze with more than one column.")
    756     values = values[0, :]  # type: ignore[call-overload]
--> 758 new_values = astype_array_safe(values, dtype, copy=copy, errors=errors)
    760 new_values = maybe_coerce_values(new_values)
    762 refs = None

File ~/cusy/trn/jupyter-tutorial/uvenvs/py313/.venv/lib/python3.13/site-packages/pandas/core/dtypes/astype.py:237, in astype_array_safe(values, dtype, copy, errors)
    234     dtype = dtype.numpy_dtype
    236 try:
--> 237     new_values = astype_array(values, dtype, copy=copy)
    238 except (ValueError, TypeError):
    239     # e.g. _astype_nansafe can fail on object-dtype of strings
    240     #  trying to convert to float
    241     if errors == "ignore":

File ~/cusy/trn/jupyter-tutorial/uvenvs/py313/.venv/lib/python3.13/site-packages/pandas/core/dtypes/astype.py:182, in astype_array(values, dtype, copy)
    179     values = values.astype(dtype, copy=copy)
    181 else:
--> 182     values = _astype_nansafe(values, dtype, copy=copy)
    184 # in pandas we don't store numpy str dtypes, so convert to object
    185 if isinstance(dtype, np.dtype) and issubclass(values.dtype.type, str):

File ~/cusy/trn/jupyter-tutorial/uvenvs/py313/.venv/lib/python3.13/site-packages/pandas/core/dtypes/astype.py:101, in _astype_nansafe(arr, dtype, copy, skipna)
     96     return lib.ensure_string_array(
     97         arr, skipna=skipna, convert_na_value=False
     98     ).reshape(shape)
    100 elif np.issubdtype(arr.dtype, np.floating) and dtype.kind in "iu":
--> 101     return _astype_float_to_int_nansafe(arr, dtype, copy)
    103 elif arr.dtype == object:
    104     # if we have a datetime/timedelta array of objects
    105     # then coerce to datetime64[ns] and use DatetimeArray.astype
    107     if lib.is_np_dtype(dtype, "M"):

File ~/cusy/trn/jupyter-tutorial/uvenvs/py313/.venv/lib/python3.13/site-packages/pandas/core/dtypes/astype.py:145, in _astype_float_to_int_nansafe(values, dtype, copy)
    141 """
    142 astype with a check preventing converting NaN to an meaningless integer value.
    143 """
    144 if not np.isfinite(values).all():
--> 145     raise IntCastingNaNError(
    146         "Cannot convert non-finite values (NA or inf) to integer"
    147     )
    148 if dtype.kind == "u":
    149     # GH#45151
    150     if not (values >= 0).all():

IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer

Fehler, wie dieser IntCastingNaNError können vermieden werden, indem durch errors = "ignore" ggf. der ursprüngliche Datentyp beibehalten wird:

[8]:
n.astype("int8", errors="ignore")
[8]:
0    90.0
1     NaN
2     1.0
dtype: float64

Die Verwendung des richtigen Typs kann Speicherplatz einsparen. Üblich ist ein 8 Byte breiter Datentyp, also int64 oder float64. Wenn ihr einen schmaleren Typ verwenden könnt, reduziert dies den Speicherverbrauch deutlich, sodass ihr mehr Daten verarbeiten könnt. Ihr könnt NumPy verwenden, um die Grenzen von Integer- und Float-Typen zu überprüfen:

[9]:
np.iinfo("int8")
[9]:
iinfo(min=-128, max=127, dtype=int8)
[10]:
np.iinfo("int64")
[10]:
iinfo(min=-9223372036854775808, max=9223372036854775807, dtype=int64)
[11]:
np.finfo("float32")
[11]:
finfo(resolution=1e-06, min=-3.4028235e+38, max=3.4028235e+38, dtype=float32)
[12]:
np.finfo("float64")
[12]:
finfo(resolution=1e-15, min=-1.7976931348623157e+308, max=1.7976931348623157e+308, dtype=float64)

Speicherverbrauch

Um den Speicherverbrauch der Series zu berechnen, könnt ihr pandas.Series.nbytes verwenden um den Speicher, der von den Daten verwendet wird, zu ermitteln. pandas.Series.memory_usage erfasst darüberhinaus auch den Indexspeicher und den Datentyp. Mit deep=True lässt sich auch der Speicherverbrauch auf Systemebene ermitteln.

[13]:
s.nbytes
[13]:
56
[14]:
s.astype("Float32").nbytes
[14]:
35
[15]:
s.memory_usage()
[15]:
188
[16]:
s.astype("Float32").memory_usage()
[16]:
167
[17]:
s.memory_usage(deep=True)
[17]:
188

String- und Kategorietypen

Die Methode pandas.Series.astype kann auch numerische Reihen in Zeichenketten umwandeln, wenn ihr str übergebt. Beachtet den dtype im folgenden Beispiel:

[18]:
s.astype(str)
[18]:
0    -1.6634723613898739
1    0.20513361124745808
2     0.3333563040239043
3    -0.6395333353979279
4     1.5197153715827265
5    0.33144403280572465
6     1.5517663730128375
dtype: object
[19]:
s.astype(str).memory_usage()
[19]:
188
[20]:
s.astype(str).memory_usage(deep=True)
[20]:
605

Zur Konvertierung in einen kategorialen Typ könnt ihr 'category' als Typ übergeben:

[21]:
s.astype(str).astype("category")
[21]:
0    -1.6634723613898739
1    0.20513361124745808
2     0.3333563040239043
3    -0.6395333353979279
4     1.5197153715827265
5    0.33144403280572465
6     1.5517663730128375
dtype: category
Categories (7, object): ['-0.6395333353979279', '-1.6634723613898739', '0.20513361124745808', '0.33144403280572465', '0.3333563040239043', '1.5197153715827265', '1.5517663730128375']

Eine kategoriale Series ist nützlich für String-Daten und kann zu großen Speichereinsparungen führen. Das liegt daran, dass pandas bei der Konvertierung in kategoriale Daten nicht länger Python-Strings für jeden Wert verwendet, sondern sich wiederholende Werte nicht dupliziert werden. Ihr habt immer noch alle Funktionen des str-Attributs, aber ihr spart viel Speicherplatz wenn ihr viele doppelte Werte habt und steigert die Leistung, da ihr nicht so viele String-Operationen durchführen müsst.

[22]:
s.astype("category").memory_usage(deep=True)
[22]:
495

Geordnete Kategorien

Um geordnete Kategorien zu erstellen, müsst ihr einen eigenen pandas.CategoricalDtype definieren:

[23]:
from pandas.api.types import CategoricalDtype


s_sorted = pd.Series(sorted(set(s)))
cat_dtype = CategoricalDtype(categories=s_sorted, ordered=True)

s.astype(cat_dtype)
[23]:
0   -1.663472
1    0.205134
2    0.333356
3   -0.639533
4    1.519715
5    0.331444
6    1.551766
dtype: category
Categories (7, float64): [-1.663472 < -0.639533 < 0.205134 < 0.331444 < 0.333356 < 1.519715 < 1.551766]
[24]:
s.astype(cat_dtype).memory_usage(deep=True)
[24]:
495

In der folgenden Tabelle sind die Typen aufgeführt, die ihr an astype übergeben könnt.

Datentyp

Beschreibung

str, 'str'

in Python-String konvertieren

'string'

in Pandas-String konvertieren mit pandas.NA

int, 'int', 'int64'

in NumPy int64 konvertieren

'int32', 'uint32'

in NumPy int32 konvertieren

'Int64'

in pandas Int64 konvertieren mit pandas.NA

float, 'float', 'float64'

in Floats konvertieren

'category'

in CategoricalDtype konvertieren mit pandas.NA

Umwandlung in andere Datentypen

Die Methode pandas.Series.to_numpy oder die Eigenschaft pandas.Series.values liefert uns ein NumPy-Array mit Werten, und pandas.Series.to_list gibt eine Python-Liste mit Werten zurück. Warum solltet ihr das wollen? pandas-Objekte sind meist viel benutzerfreundlicher und der Code lässt sich leichter lesen. Zudem werden Python-Listen sehr viel langsamer verarbeitet werden können. Mit pandas.Series.to_frame könnt ihr ggf. einen DataFrame mit einer einzigen Spalte erzeugen:

[25]:
s.to_frame()
[25]:
0
0 -1.663472
1 0.205134
2 0.333356
3 -0.639533
4 1.519715
5 0.331444
6 1.551766

Auch die Funktion pandas.to_datetime kann hilfreich sein um in pandas um Werte in Datum und Uhrzeit zu konvertieren.