Tengo un DataFrame de algunas transacciones. Quiero agrupar estas transacciones con respecto a sus valores de columna item y time: el objetivo es agrupar los elementos que están dentro de 1 hora entre sí. Entonces, comenzamos un nuevo grupo en el momento de la siguiente observación que no estaba dentro de una hora de la observación anterior (Ver columna start time en DataFrame B).

Aquí están los datos: Quiero convertir A a B.

A=
item    time             result
A   2016-04-18 13:08:25  Y
A   2016-04-18 13:57:05  N
A   2016-04-18 14:00:12  N
A   2016-04-18 23:45:50  Y
A   2016-04-20 16:53:48  Y
A   2016-04-20 17:11:47  N
B   2016-04-18 15:24:48  N
C   2016-04-23 13:20:44  N
C   2016-04-23 14:02:23  Y


B=
item    start time            end time      Ys  Ns  total count
A   2016-04-18 13:08:25 2016-04-18 14:08:25 1   2   3
A   2016-04-18 23:45:50 2016-04-18 00:45:50 1   0   1
A   2016-04-20 16:53:48 2016-04-20 17:53:48 1   1   2
B   2016-04-18 15:24:48 2016-04-18 16:24:48 0   1   1
C   2016-04-23 13:20:44 2016-04-23 14:20:44 1   1   2

Esto es lo que hice:

grouped = A.groupby('item')
A['end'] = (grouped['time'].transform(lambda grp: grp.min()+pd.Timedelta(hours=1)))
A2 = A.loc[(A['time'] <= A['end'])]

Esto me da un grupo por día: la transacción dentro de 1 hora de la primera transacción. Por lo tanto, me faltan otras transacciones en el mismo día pero a más de 1 hora de la primera. Mi lucha es cómo conseguir esos grupos. Entonces puedo usar pd.crosstab para obtener los detalles que quiero de la columna result.

Otra idea que tengo es ordenar A por item y time, y luego ir fila por fila. Si el tiempo está dentro de 1 hora de la fila anterior, se agrega a ese grupo, de lo contrario, crea un nuevo grupo.

3
Ana 11 may. 2016 a las 20:33

3 respuestas

La mejor respuesta

1) Configure una columna window_end para su uso posterior con .groupby(), y defina .get_windows() para verificar, para cada grupo item, si un row se ajusta al actual ventana actual de 1 hora, o no hacer nada y mantener el valor inicializado. Aplicar a todos los grupos item:

df['window_end'] = df.time + pd.Timedelta('1H')

def get_windows(data):
    window_end = data.iloc[0].window_end
    for index, row in data.iloc[1:].iterrows():
        if window_end > row.time:
            df.loc[index, 'window_end'] = window_end
        else:
            window_end = row.window_end

df.groupby('item').apply(lambda x: get_windows(x))

2) Utilice windows y item con .groupby() y devuelva .value_counts() como transposed DataFrame, limpie index y agregue { {X7}}:

df = df.groupby(['window_end', 'item']).result.apply(lambda x: x.value_counts().to_frame().T)
df = df.fillna(0).astype(int).reset_index(level=2, drop=True)
df['total'] = df.sum(axis=1)

Llegar:

                            N  Y  total
window_end          item               
2016-04-18 14:08:25 A    A  2  1      3
2016-04-18 16:24:48 B    B  1  0      1
2016-04-19 00:45:50 A    A  0  1      1
2016-04-20 17:53:48 A    A  1  1      2
2016-04-23 14:20:44 C    C  1  1      2
1
Stefan 11 may. 2016 a las 21:31

Inspirado (+1) por la solución de Stefan llegué a este:

B = (A.groupby(['item', A.groupby('item')['time']
                         .diff().fillna(0).dt.total_seconds()//60//60
               ],
               as_index=False)['time'].min()
)


B[['N','Y']] = (A.groupby(['item', A.groupby('item')['time']
                                    .diff().fillna(0).dt.total_seconds()//60//60
                          ])['result']
                 .apply(lambda x: x.value_counts().to_frame().T).fillna(0)
                 .reset_index()[['N','Y']]
)

Salida:

In [178]: B
Out[178]:
  item                time    N    Y
0    A 2016-04-18 13:08:25  3.0  1.0
1    A 2016-04-18 23:45:50  0.0  1.0
2    A 2016-04-20 16:53:48  0.0  1.0
3    B 2016-04-18 15:24:48  1.0  0.0
4    C 2016-04-23 13:20:44  1.0  1.0

PD: la idea es utilizar A.groupby('item')['time'].diff().fillna(0).dt.total_seconds()//60//60 como parte de la agrupación:

In [179]: A.groupby('item')['time'].diff().fillna(0).dt.total_seconds()//60//60
Out[179]:
0     0.0
1     0.0
2     0.0
3     9.0
4    41.0
5     0.0
6     0.0
7     0.0
8     0.0
Name: time, dtype: float64
1
MaxU 11 may. 2016 a las 19:04

Preparar

import pandas as pd
from StringIO import StringIO

text = """item    time             result
A   2016-04-18 13:08:25  Y
A   2016-04-18 13:57:05  N
A   2016-04-18 14:00:12  N
A   2016-04-18 23:45:50  Y
A   2016-04-20 16:53:48  Y
A   2016-04-20 17:11:47  N
B   2016-04-18 15:24:48  N
C   2016-04-23 13:20:44  N
C   2016-04-23 14:02:23  Y
"""

df = pd.read_csv(StringIO(text), delimiter="\s{2,}", parse_dates=[1], engine='python')

Solución

Necesitaba crear algunas funciones de proceso:

def set_time_group(df):
    cur_time = pd.NaT
    for index, row in df.iterrows():
        if pd.isnull(cur_time):
            cur_time = row.time
        delta = row.time - cur_time
        if delta.seconds / 3600. < 1:
            df.loc[index, 'time_ref'] = cur_time
        else:
            df.loc[index, 'time_ref'] = row.time
            cur_time = row.time
    return df

def summarize_results(df):
    df_ = df.groupby('result').count().iloc[:, 0]
    df_.loc['total count'] = df_.sum()
    return df_

dfg1 = df.groupby('item').apply(set_time_group)
dfg2 = dfg1.groupby(['item', 'time_ref']).apply(summarize_results)
df_f = dfg2.unstack().fillna(0)

Demostración

print df_f

result                      N    Y  total count
item time_ref                                  
A    2016-04-18 13:08:25  2.0  1.0          3.0
     2016-04-18 23:45:50  0.0  1.0          1.0
     2016-04-20 16:53:48  1.0  1.0          2.0
B    2016-04-18 15:24:48  1.0  0.0          1.0
C    2016-04-23 13:20:44  1.0  1.0          2.0
1
piRSquared 11 may. 2016 a las 19:04