суммирование иерархических данных с использованием подстановочных строк - PullRequest
2 голосов
/ 05 февраля 2020

Даны иерархические данные, где каждая запись имеет основную категорию f1, подкатегорию f2, подкатегорию f3 и результат y. Например, мы могли бы посмотреть на данные о человеке, где каждая запись соответствует уникальному человеку, f1 = страна, f2 = провинция, f3 = город, где родился человек, и y = двоичное значение, указывающее, имеет ли человек конкретный c ген. Вот небольшой пример:

   f1 f2 f3  y
0   1  A  a  0
1   1  A  b  1
2   1  B  c  1
3   1  B  a  1
4   2  A  a  0
5   2  A  c  0
6   2  B  d  0

То, что я хотел бы сделать, это обобщить эти данные на основе результата y, используя подстановочные знаки '*'.

   f1 f2 f3  y s
0   1  A  a  0 1.A.a
1   1  A  b  1 1.A.b
2   1  B  c  1 1.B.*
3   1  B  a  1 1.B.*
4   2  A  a  0 2.*.*
5   2  A  c  0 2.*.*
6   2  B  d  0 2.*.*

В приведенной выше таблице я создал сводный столбец s. Здесь записи 4, 5 и 6 представлены как '2.*.*', поскольку все записи с f1 = 2 имеют y = 0, независимо от подкатегорий f2 и f3. Аналогично, 2 и 3 могут быть представлены как '1.B.*', поскольку результат y для f1 = 1, f2 = B всегда равен 1, независимо от значения f3. Крайний крайний случай возникает, если все результаты у одинаковы. В этом случае мы представили бы каждую запись как *.*.*.

Вопрос в том, как эффективно построить этот итоговый столбец. Я полагаю, что могу каким-то образом добиться этого, используя групповые выражения, но я не совсем уверен, как. Обратите внимание, что этот пример ограничен 3 категориями f1, f2, f3 и y ограничен двоичным, но на практике может быть больше категорий и больше возможных значений результата в y.

Код для создания Таблица выше:

import pandas as pd
df = pd.DataFrame({'f1': [1,1,1,1,2,2,2],
                   'f2': ['A','A','B','B','A','A','B'],
                   'f3': ['a','b','c','a','a','c','d'],
                   'y': [0,1,1,1,0,0,0]})
print(df)

Ответы [ 2 ]

0 голосов
/ 06 февраля 2020

Не самая адаптируемая реализация, но сделает свое дело - и будет быстрее, чем for l oop;)

df[["f1", "f2", "f3"]]=df[["f1", "f2", "f3"]].astype(str)

#count unique y-s for different groups.

dfgr1=df.groupby("f1")["y"].nunique()
dfgr2=df.groupby(["f1", "f2"])["y"].nunique()

#build summary based on cascading number of unique elements within different groups

df["s"]=np.where(dfgr1.loc[df["f1"]].eq(1), df["f1"]+".*.*", np.where(dfgr2.loc[zip(df["f1"], df["f2"])].eq(1), df["f1"].str.cat(df["f2"], sep=".")+".*", df["f1"].str.cat(df[["f2", "f3"]], sep=".")))

Выходы:

  f1 f2 f3  y      s
0  1  A  a  0  1.A.a
1  1  A  b  1  1.A.b
2  1  B  c  1  1.B.*
3  1  B  a  1  1.B.*
4  2  A  a  0  2.*.*
5  2  A  c  0  2.*.*
6  2  B  d  0  2.*.*
0 голосов
/ 06 февраля 2020

Следующий код достигает желаемой цели. Код сначала строит дерево решений для назначения правильной метки каждой строке. Это дерево вычисляется рекурсивным способом. Как только дерево вычислено, функция relbel использует это дерево для вычисления нужных меток для предоставленного фрейма данных.

Рекурсивная функция comp_label_map строит дерево решений. Он рекурсивно разбивает данные на более мелкие блоки, пока для данного блока все значения y не будут одинаковыми.

import pandas as pd

# Compute a decision tree that can be used to map rows to their final labels
def comp_label_map(df, categories, goal, prefix, index):
    # Base case: all rows in the df block have the same goal (y) value.
    if df[goal].nunique()==1:
        return prefix+"".join(['*.']*(len(categories)-index))
    if index > len(categories):
        raise RuntimeError('Ran out of categories')

    # else, we must have different values, keep splitting
    groups=df.groupby(categories[index])
    level = {}
    for name, group in groups:
        level[name]=comp_label_map(group,categories,goal,prefix+str(name)+'.',index+1)
    return level

def relabel(row, categories,relabel_map):
    tree_node=relabel_map['_']
    for category in categories:
        #check whether tree_node is a dict. If not we reached a leaf of the decision tree
        if not isinstance(tree_node,dict): 
            return tree_node
        #not a leaf, we must check whether we can go deeper. Test wheter theres a mapping for this category
        if not row[category] in tree_node:
            return 'Not Defined'

        tree_node=tree_node[row[category]]

    return tree_node


df = pd.DataFrame({'f1': [1,1,1,1,2,2,2],
                   'f2': ['A','A','B','B','A','A','B'],
                   'f3': ['a','b','c','a','a','c','d'],
                   'y': [0,1,1,1,0,0,0]})

print(df) #df before
relabel_map={ '_':comp_label_map(df,['f1','f2','f3'],'y', '', 0)}
df['relabeled']=df.apply(lambda row: relabel(row, ['f1','f2','f3'], relabel_map),axis=1)
print(relabel_map)
print(df) #df after

Результат:

   f1 f2 f3  y s
0   1  A  a  0 1.A.a
1   1  A  b  1 1.A.b
2   1  B  c  1 1.B.*
3   1  B  a  1 1.B.*
4   2  A  a  0 2.*.*
5   2  A  c  0 2.*.*
6   2  B  d  0 2.*.*

Соответствующее дерево решений:

{'_': 
    {1: 
        {'A': 
            {'a': '1.A.a.', 
             'b': '1.A.b.'
            }, 
         'B': '1.B.*.'
        }, 
     2: '2.*.*.'
    }
}

Примечание: самый первый ключ _ действует как root узел. Если все значения y одинаковы во всех строках, тогда дерево решений будет просто {'_':'*.*.*}.

...