Aquí hay un ejemplo de lo que estoy tratando de hacer:

   bar  foo   o1   o2 thing
0    1    1  0.0  3.3     a
1    1    1  1.1  4.4     a
2    3    2  2.2  5.5     b
   foo_1_bar_3_o1  foo_1_bar_3_o2  foo_2_bar_3_o1  foo_2_bar_3_o2  \
0             NaN             NaN             NaN             NaN   
1             NaN             NaN             2.2             5.5   

   foo_1_bar_1_o1  foo_1_bar_1_o2  foo_2_bar_1_o1  foo_2_bar_1_o2 thing  
0             1.1             7.7             NaN             NaN     a  
1             NaN             NaN             NaN             NaN     b  

El primero es mi DataFrame de entrada y el segundo es mi DataFrame de salida deseado (NaN s pueden ser sustituidos por 0's).

Esto debería ser una especie de groupby (en la columna thing) y luego algún tipo de función de agregación de los valores en las columnas o1 y o2 que se agregan en función de todas las combinaciones posibles de valores de foo y bar. Observe que foo_1_bar_2_o2 es 7.7 porque es la suma de la columna o2 cuando foo == 1 && bar == 2 para el grupo 'a'.

Intenté investigar dcast, crosstab y pivot en pandas, pero ninguno parece satisfacer lo que estoy tratando de hacer.

Escribí código base de Python que hace lo que quiero, pero, de nuevo, me gustaría traducirlo a un formato más amigable usando funciones ya existentes. No creo que mi caso de uso sea lo suficientemente oscuro como para que esto no sea posible.

A continuación se muestra el código Python base para esta operación.

import pandas as pd
import numpy as np
import itertools

df = pd.DataFrame({'thing': ['a', 'a', 'b'], 
                   'foo': [1, 1, 2], 
                   'bar': [1, 1, 3], 
                   'o1': [0.0, 1.1, 2.2], 
                   'o2': [3.3, 4.4, 5.5]})

key_columns = ['foo', 'bar']

key_value_pairs = [df[key].values.tolist() for key in key_columns]

key_value_pairs = list(set(itertools.product(*key_value_pairs)))

output_columns = ['o1', 'o2']

def aggregate(df):
  new_columns = []
  for pair in key_value_pairs:
    pair = list(zip(key_columns, pair))
    new_column = '_'.join(['%s_%d' % (key, value) for key, value in pair])
    for o in output_columns:
      criteria = list()
      for key, value in pair:
        criterion = (df[key] == value)
        criteria.append(criterion)
      new_columns.append('%s_%s' % (new_column, o))
      df[new_columns[-1]] = df[np.logical_and.reduce(criteria)][o].sum()
  return df.head(1)[new_columns + ['thing']]

things = df['thing'].value_counts().index.tolist()

groups = df.groupby('thing')

dfs = []
for thing in things:
  dfs.append(aggregate(groups.get_group(thing).reset_index()))
  #print(aggregate(groups.get_group(thing).reset_index(drop=True)))

print(df)
print(pd.concat(dfs).reset_index(drop=True))
2
coneyhelixlake 13 nov. 2017 a las 09:40

2 respuestas

La mejor respuesta

Creo que aún tendrás que usar itertools.product(), porque Pandas no está diseñado para pensar en datos que no existen. Pero una vez que haya definido esas combinaciones adicionales, puede usar groupby() y unstack() para obtener el resultado que está buscando.

Usando el key_value_pairs que definiste:

for k,v in key_value_pairs:
    if not len(df.loc[df.foo.eq(k) & df.bar.eq(v)]):
        df = df.append({"foo":k, "bar":v, "o1":np.nan, "o2":np.nan, "thing":"a"}, ignore_index=True)
        df = df.append({"foo":k, "bar":v, "o1":np.nan, "o2":np.nan, "thing":"b"}, ignore_index=True)

df
   bar  foo   o1   o2 thing
0    1    1  0.0  3.3     a
1    1    1  1.1  4.4     a
2    3    2  2.2  5.5     b
3    3    1  NaN  NaN     a
4    3    1  NaN  NaN     b
5    1    2  NaN  NaN     a
6    1    2  NaN  NaN     b

Ahora groupby y unstack:

gb = df.groupby(["thing", "foo", "bar"]).sum().unstack(level=[1,2])
gb.columns = [f"foo_{b}_bar_{c}_{a}" for a,b,c in gb.columns]

Salida:

       foo_1_bar_1_o1  foo_1_bar_3_o1  foo_2_bar_1_o1  foo_2_bar_3_o1  \
thing                                                                   
a                 1.1             NaN             NaN             NaN   
b                 NaN             NaN             NaN             2.2   

       foo_1_bar_1_o2  foo_1_bar_3_o2  foo_2_bar_1_o2  foo_2_bar_3_o2  
thing                                                                  
a                 7.7             NaN             NaN             NaN  
b                 NaN             NaN             NaN             5.5  
1
andrew_reece 13 nov. 2017 a las 08:06

Intento crear una solución dinámica:

key_columns = ['foo', 'bar']
output_columns = ['o1', 'o2']

Primero agregue key_columns cadenas a los valores con { {X1}}:

df[key_columns] = (df[key_columns].astype(str)
                                  .radd(pd.Series(key_columns,index=key_columns) + '_'))

print (df)
     bar    foo   o1   o2 thing
0  bar_1  foo_1  0.0  3.3     a
1  bar_1  foo_1  1.1  4.4     a
2  bar_3  foo_2  2.2  5.5     b

Luego, agregue por sum y remodelado por unstack - obtenga MultiIndex en columnas:

df = df.groupby(['thing'] + key_columns)[output_columns].sum().unstack(key_columns)
print (df)
         o1          o2      
bar   bar_1 bar_3 bar_1 bar_3
foo   foo_1 foo_2 foo_1 foo_2
thing                        
a       1.1   NaN   7.7   NaN
b       NaN   2.2   NaN   5.5

Cree todas las combinaciones posibles con MultiIndex.from_product para reindex, luego reorder_levels y sort_index:

mux = pd.MultiIndex.from_product(df.columns.levels, names=df.columns.names)
print (mux)
MultiIndex(levels=[['o1', 'o2'], ['foo_1', 'foo_2'], ['bar_1', 'bar_3']],
           labels=[[0, 0, 0, 0, 1, 1, 1, 1], [0, 0, 1, 1, 0, 0, 1, 1],
                   [0, 1, 0, 1, 0, 1, 0, 1]],
           names=[None, 'foo', 'bar'])


df = df.reindex(columns=mux).reorder_levels(key_columns + [None], axis=1).sort_index(axis=1)

Última eliminación de MultiIndex por map con join:

df.columns = df.columns.map('_'.join)
df = df.reset_index()
print (df)
  thing  foo_1_bar_1_o1  foo_1_bar_1_o2  foo_1_bar_3_o1  foo_1_bar_3_o2  \
0     a             1.1             7.7             NaN             NaN   
1     b             NaN             NaN             NaN             NaN   

   foo_2_bar_1_o1  foo_2_bar_1_o2  foo_2_bar_3_o1  foo_2_bar_3_o2  
0             NaN             NaN             NaN             NaN  
1             NaN             NaN             2.2             5.5  
2
jezrael 13 nov. 2017 a las 08:29