DEV Community

wrighter
wrighter

Posted on • Originally published at wrighters.io on

Converting types in Pandas

Pandas is great for dealing with both numerical and text data. In most projects you’ll need to clean up and verify your data before analysing or using it for anything useful. Data might be delivered in databases, csv or other formats of data file, web scraping results, or even manually entered. Once you have loaded data into pandas, you’ll likely need to convert it to a type that makes the most sense for what you are trying to accomplish. In this post, I’m going to review the basic datatypes in pandas and how to safely and accurately convert data.

DataFrame and Series

First, let’s review the basic container types in pandas, Series and DataFrame. A Series is a one dimensional labeled array of data, backed by a NumPy array. A DataFrame is a two-dimensional structure that consists of multiple Series columns that share an index. A Series has a data type, referenced as dtype, and all elements in that Series will share the same type.

But what types?

The data type can be a core NumPy datatype, which means it could be a numerical type, or Python object. But the type can also be a pandas extension type, known as an ExtensionDType. Without getting into too much detail, just know two very common examples are the CategoricalDType, and in pandas 1.0+, the StringDType. For now, what’s important to remember is that all elements in a Series share the same type.

What’s important to realize is that when constructiong a Series or a DataFrame, pandas will pick the datatype that can represent all values in the Series (or DataFrame). Let’s look at an example to make this more clear. Note, this example was run using pandas version 1.1.4.

>>> import pandas as pd
>>> s = pd.Series([1.0, 'N/A', 2])
>>> s
0 1
1 N/A
2 2
dtype: object
Enter fullscreen mode Exit fullscreen mode

As you can see, pandas has chosen the object type for my Series since it can represent values that are floating point numbers, strings, and integers. The individual items in this Series are all of a different type in this case, but can be represented as objects.

>>> print(type(s[0]))
<class 'float'>
>>> print(type(s[1]))
<class 'str'>
>>> print(type(s[2]))
<class 'int'>
Enter fullscreen mode Exit fullscreen mode

So, what’s the problem?

The problem with using object for everything is that you rarely want to work with your data this way. Looking at this first example, if you had imported this data from a text file you’d most likely want it to be treated as numerical, and perhaps calculate some statistical values from it.

>>> try:
... s.mean()
... except Exception as ex:
... print(ex)
...
unsupported operand type(s) for +: 'float' and 'str'
Enter fullscreen mode Exit fullscreen mode

It’s clear here that the mean function fails because it’s trying to add up the values in the Series and cannot add the ‘N/A’ to the running sum of values.

So how do we fix this?

Well, we could inspect the values and convert them by hand or using some other logic, but luckily pandas gives us a few options to do this in a sensible way. Let’s go through them all.

astype

First, you can try to use astype to convert values. astype is limited, however, because if it cannot convert a value it will either raise an error or return the original value. Because of this, it cannot completely help us in this situation.

>>> try:
... s.astype('float')
... except Exception as ex:
... print(ex)
...
could not convert string to float: 'N/A'
Enter fullscreen mode Exit fullscreen mode

But astype is very useful, so before moving on, let’s look at a few examples where you would use it. First, if your data was all convertible between types, it would do just what you want.

>>> s2 = pd.Series([1, "2", "3.4", 5.5])
>>> print(s2)
0 1
1 2
2 3.4
3 5.5
dtype: object
>>> print(s2.astype('float'))
0 1.0
1 2.0
2 3.4
3 5.5
dtype: float64
Enter fullscreen mode Exit fullscreen mode

Second, astype is useful for saving space in Series and DataFrames, especially when you have repeated types that can be expressed as categoricals. Categoricals can save memory and also make data a little more readable during analysis since it will tell you all the possible values. For example:

>>> s3 = pd.Series(["Black", "Red"] * 1000)
>>>
>>> s3.astype('category')
0 Black
1 Red
2 Black
3 Red
4 Black
        ...
1995 Red
1996 Black
1997 Red
1998 Black
1999 Red
Length: 2000, dtype: category
Categories (2, object): ['Black', 'Red']
>>>
>>> print("String:", s3.memory_usage())
String: 16128
>>> print("Category:", s3.astype('category').memory_usage())
Category: 2224
>>>
Enter fullscreen mode Exit fullscreen mode

You can also save space by using smaller NumPy types.

>>> s4 = pd.Series([22000, 3, 1, 9])
>>>s4.memory_usage()
160
>>> s4.astype('int8').memory_usage()
132
Enter fullscreen mode Exit fullscreen mode

But note there is an error above! astype will happily convert numbers that don’t fit in the new type without reporting the error to you.

>>> s4.astype('int8')
0 -16
1 3
2 1
3 9
dtype: int8
Enter fullscreen mode Exit fullscreen mode

Note that you can also use astype on DataFrames, even specifying different values for each column

>>> df = pd.DataFrame({'a': [1,2,3.3, 4], 'b': [4, 5, 2, 3], 'c': ["4", 5.5, "7.09", 1]})
>>> df.astype('float')
     a b c
0 1.0 4.0 4.00
1 2.0 5.0 5.50
2 3.3 2.0 7.09
3 4.0 3.0 1.00
>>> df.astype({'a': 'uint', 'b': 'float16'})
   a b c
0 1 4.0 4
1 2 5.0 5.5
2 3 2.0 7.09
3 4 3.0 1
Enter fullscreen mode Exit fullscreen mode

to_numeric (or to_datetime or to_timedelta)

There are a few better options available in pandas for converting one-dimensional data (i.e. one Series at a time). These methods provide better error correction than astype through the optional errors and downcast parameters. Take a look at how it can deal with the first Series created in this post. Using coerce for errors will turn any conversion errors into NaN. Passing in ignore will get the same behavior we had available in astype, returning our original input. Likewise, passing in raise will raise an exception.

>>> pd.to_numeric(s, errors='coerce')
0 1.0
1 NaN
2 2.0
dtype: float64
Enter fullscreen mode Exit fullscreen mode

And if we want to save some space, we can safely downcast to the minimim size that will hold our data without errors (getting int16 instead of int64 if we didn’t downcast)

>>> pd.to_numeric(s4, downcast='integer')
0 22000
1 3
2 1
3 9
dtype: int16
>>> pd.to_numeric(s4).dtype
dtype('int64')
Enter fullscreen mode Exit fullscreen mode

The to_datetime and to_timedelta methods will behave similarly, but for dates and timedeltas.

>>> pd.to_numeric(s4).dtype
dtype('int64')
>>> pd.to_timedelta(['2 days', '5 min', '-3s', '4M', '1 parsec'], errors='coerce')
TimedeltaIndex([ '2 days 00:00:00', '0 days 00:05:00', '-1 days +23:59:57',
                  '0 days 00:04:00', NaT],
               dtype='timedelta64[ns]', freq=None)
>>> pd.to_datetime(['11/1/2020', 'Jan 4th 1919', '20200930 08:00:31'])
DatetimeIndex(['2020-11-01 00:00:00', '1919-01-04 00:00:00',
               '2020-09-30 08:00:31'],
              dtype='datetime64[ns]', freq=None)
Enter fullscreen mode Exit fullscreen mode

Since these functions are all for 1-dimensional data, you will need to use apply on a DataFrame. For instance, to downcast all the values to the smallest possible floating point size, use the downcast parameter.

>>> from functools import partial
>>> df.apply(partial(pd.to_numeric, downcast='float')).dtypes
a float32
b float32
c float32
dtype: object
Enter fullscreen mode Exit fullscreen mode

infer_objects

If you happend to have a pandas object that consists of objects that haven’t been converted yet, both Series and DataFrame have a method that will attempt to convert those objects to the most sensible type. To see this, you have to do a sort of contrived example, because pandas will attempt to convert objects when you create them. For example:

>>> pd.Series([1, 2, 3, 4], dtype='object').infer_objects().dtype
int64
>>> pd.Series([1, 2, 3, '4'], dtype='object').infer_objects().dtype
object
>>>pd.Series([1, 2, 3, 4]).dtype
int64
Enter fullscreen mode Exit fullscreen mode

You can see here that if the Series happens to have all numerical types (in this case integers) but they are stored as objects, it can figure out how to convert these to integers. But it doesn’t know how to convert the ‘4’ to an integer. For that, you need to use one of the techniques from above.

convert_dtypes

This method is new in pandas 1.0, and can convert to the best possible dtype that supports pd.NA. Note that this will be the pandas dtype versus the NumPy dtype (i.e. Int64 instead of int64).

>>> pd.Series([1, 2, 3, 4], dtype='object').convert_dtypes().dtype
Int64
>>> pd.Series([1, 2, 3, '4'], dtype='object').convert_dtypes().dtype
object
>>> pd.Series([1, 2, 3, 4]).convert_dtypes().dtype
Int64
Enter fullscreen mode Exit fullscreen mode

What should you use most often then?

What I recommend doing is looking at your raw data once it is imported. Depending on your data source, it may already be in the dtype that you want. But once you need to convert it, you have all the tools you need to do this correctly. For numeric types, the pd.to_numeric method is best suited for doing this conversion in a safe way, and with wise use of the downcast parameter, you can also save space. Consider using astype("category") when you have repeated data to save some space as well. The convert_dtypes and infer_objects methods are not going to be that helpful in most cases unless you somehow have data stored as objects that is readily convertible to another type. Remember, there’s no magic function in pandas that will ensure you have the best data type for every case, you need to examine and understand your own data to use or analyze it correctly. But knowing the best way to do that conversion is a great start.

Discussion (0)