Pandas Timedelta mean returns error "No numeric types to aggregate". Why?

Viewed 800

I am trying to perform the following operation:

pd.concat([A,B], axis = 1).groupby("status_reason")["closing_time"].mean()

Where

  • A is a Series named "status_reason" (Categorical values)
  • B is a Series named "closing_time" (TimeDelta values)

Example:

In : A.head(5)
Out: 
     0    -1 days +11:35:00
     1   -10 days +07:13:00
     2                  NaT
     3                  NaT
     4                  NaT
    Name: closing_time, dtype: timedelta64[ns]

In : B.head(5)
Out:
     0            Won
     1       Canceled
     2    In Progress
     3    In Progress
     4    In Progress
     Name: status_reason, dtype: object

The following error occurs:

DataError: No numeric types to aggregate

Please note: I tried to perform the mean even isolating every single category

Now, I saw a few question similar to mine online, so I tried this:

pd.to_timedelta(pd.concat([pd.to_numeric(A),B], axis = 1).groupby("status_reason")["closing_time"].mean())

Which is simply converting the Timedelta to an int64 and viceversa. But there result was quite strange (numbers too high)

In order to investigate the situation, I wrote the following code:

xxx = pd.concat([A,B], axis = 1)
xxx.closing_time.mean()
#xxx.groupby("status_reason")["closing_time"].mean()

The second row WORKS FINE, without converting the Timedelta to Int64. The third row DOES NOT work, and returns again the DataError.

I'm so confused here! What am I missig?

I would like to see the mean of the "closing times" for each "status reason"!

EDIT

If I try to do this: (Isolate the rows with a specific status without grouping)

yyy = xxx[xxx["status_reason"] == "In Progress"]
yyy["closing_time"].mean()

The result is:

Timedelta('310 days 21:18:05.454545')

But if I do this: (Isolate the rows with a specific status grouping)

yyy = xxx[xxx["status_reason"] == "In Progress"]
yyy.groupby("status_reason")["closing_time"].mean()

The result is again:

DataError: No numeric types to aggregate

Lastly, if I do this: (converting and converting back) (LET's CALL THIS: Special Example)

yyy = xxx[xxx["status_reason"] == "In Progress"]
yyy.closing_time = pd.to_numeric (yyy.closing_time)
pd.to_timedelta(yyy.groupby("status_reason")["closing_time"].mean())

We go back to the first problem I noticed:

status_reason
In Progress   -105558 days +10:08:05.605064
Name: closing_time, dtype: timedelta64[ns]

EDIT2

If I do this: (convert to seconds and convert back)

yyy = xxx[xxx["status_reason"] == "In Progress"]
yyy.closing_time = A.dt.seconds
pd.to_timedelta(yyy.groupby("status_reason")["closing_time"].mean(), unit="s" )

The result is

status_reason
In Progress   08:12:38.181818
Name: closing_time, dtype: timedelta64[ns]

The same result happens if I remove the NaNs, or if I fill them with 0:

yyy = xxx[xxx["status_reason"] == "In Progress"].dropna()
yyy.closing_time = A.dt.seconds
pd.to_timedelta(yyy.groupby("status_reason")["closing_time"].mean(), unit="s" )

BUT the numbers are very different from what we saw in the first edit! (Special Example)

-105558 days +10:08:05.605064

Also, let me run the same code (Special Example) with dropna():

310 days 21:18:05.454545

And again, let's run the same code (Special Example) with fillna(0):

3 days 11:14:22.819472

This is going nowhere. I should probably prepare an export of those data, and post them somewhere: Here we go

4 Answers

From reading the discussion of this issue on Github here, you can solve this issue by specifying numeric_only=False for mean calculation as follows

pd.concat([A,B], axis = 1).groupby("status_reason")["closing_time"] \
    .mean(numeric_only=False)

The problem might be In Progress only have NaT time, which might not allowed in groupby().mean(). Here's the test:

df = pd.DataFrame({'closing_time':['11:35:00', '07:13:00', np.nan,np.nan, np.nan],
                   'status_reason':['Won','Canceled','In Progress', 'In Progress', 'In Progress']})
df.closing_time = pd.to_timedelta(df.closing_time)
df.groupby('status_reason').closing_time.mean()

gives the exact error. To overcome this, do:

def custom_mean(x):
    try:
        return x.mean()
    except:
        return pd.to_timedelta([np.nan])

df.groupby('status_reason').closing_time.apply(custom_mean)

which gives:

status_reason
Canceled      07:13:00
In Progress        NaT
Won           11:35:00
Name: closing_time, dtype: timedelta64[ns]

I cannot say why groupby's mean() method does not work, but the following slight modification of your code should work: First, convert timedelta column to seconds with total_seconds() method, then groupby and mean, then convert seconds to timedelta again:

pd.to_timedelta(pd.concat([ A.dt.total_seconds(), B], axis = 1).groupby("status_reason")["closing_time"].mean(), unit="s")

For example dataframe below, the code -

df = pd.DataFrame({'closing_time':['2 days 11:35:00', '07:13:00', np.nan,np.nan, np.nan],'status_reason':['Won','Canceled','In Progress', 'In Progress', 'In Progress']})

df.loc[:,"closing_time"] = \
          pd.to_timedelta(df.closing_time).dt.days*24*3600 \
          + pd.to_timedelta(df.closing_time).dt.seconds

# or alternatively use total_seconds() to get total seconds in timedelta as follows
# df.loc[:,"closing_time"] = pd.to_timedelta(df.closing_time).dt.total_seconds()

pd.to_timedelta(df.groupby("status_reason")["closing_time"].mean(), unit="s")

produces

status_reason
Canceled      0 days 07:13:00
In Progress               NaT
Won           2 days 11:35:00
Name: closing_time, dtype: timedelta64[ns]

After a few investigation, here is what I found:

Most of the confusion comes from the fact that in one case I was calling SeriesGroupBy.mean() and in the other case Series.mean()

These functions are actually different and have different behaviours. I was not realizing that

The second important point is that converting to numeric, or to seconds, leads to a totally different behaviour when it comes to handling NaNs value.

To overcome this situation, the first thing you have to do is deciding how to handle NaN values. The best approach depends on what we want to achieve. In my case, it's fine to have even a simple categorical result, so I can do something like this:

import datetime

def define_time(row):
    if pd.isnull(row["closing_time"]):
        return "Null"
    elif row["closing_time"] < datetime.timedelta(days=100):
        return "<100"
    elif row["closing_time"] > datetime.timedelta(days=100):
        return ">100"


time_results = pd.concat([A,B], axis = 1).apply(lambda row:define_time(row), axis = 1)

In the end the result is like this:

In : 
    time_results.value_counts()
Out : 
    >100    1452
    <100    1091
    Null    1000
    dtype: int64
Related