Сгущение столбцов в фрейме данных Pandas

#python #pandas #list #dataframe #dictionary

Вопрос:

Я работаю с данными из проекта Veris через VerisPy и испытываю некоторые проблемы с их переформатированием для использования в других приложениях.

VerisPy выводит фрейм данных, содержащий проанализированную информацию о событиях из файлов JSON, отправленных в Veris. Каждая строка в кадре данных соответствует событию с подробной информацией о том, как/когда оно произошло. Проблема в том, что в итоге получаются тысячи столбцов с логическими флагами.

Таким образом, в каждой строке события могут быть такие столбцы, как:

идентификатор события действие.вредоносное ПО Экшен.взлом действие.злоупотребление актер.внешний.конкурент актер.внешнее.преступление актер.внутренний.сотрудник actor.internal.customer target
1 Правда Ложный Ложный Ложный Ложный Правда False Microsoft
2 Ложный Правда Ложный Правда Ложный Ложный False Bank of America

Я бы хотел свернуть/объединить их и использовать значения столбцов в качестве фактических данных. В приведенном выше примере это может привести к тому, что мы будем:

идентификатор события Экшен Актер цель
1 Вредоносная программа Внутренний — Сотрудник Майкрософт
2 Взлом Внешний конкурент Банк Америки

Я новичок в Pandas, но перепробовал довольно много вещей из документации со смешанными результатами. Melt казался многообещающим, но я не уверен, как включить имена столбцов в фактические данные на уровне строк. У кого-нибудь есть указатели?

Ответ №1:

в одну сторону:

  1. Установите ['event_id', 'target'] как index .
  2. split и expand columns для создания hierarchical columns (нам это понадобится в продукте dot).
  3. extract level 1 значения из hierarchical columns и делай dot product с dataframe помощью .
  4. Сделайте некоторые string manipulations и rename columns (я добавил 2 способа переименования столбцов), чтобы получить желаемое output .
 df1 = df.set_index(['event_id', 'target'])
df1.columns = df1.columns.str.split('.', expand=True, n=1)
df = (
    df1.dot(df1.columns.get_level_values(1)   ',')
    .str.strip(',')
    .str.split(',', expand=True)
    .rename(columns={0: 'Action', 1: 'Actor'})
    .reset_index()
)
 

выход:

    event_id           target   Action                Actor
0         1        Microsoft  malware    internal.employee
1         2  Bank of America  hacking  external.competitor
 
записка:

Вместо renaming columns via rename вы также можете использовать level=0 значения столбцов, подобные этому —

 df1 = df.set_index(['event_id', 'target'])
df1.columns = df1.columns.str.split('.', expand=True, n=1)
df = (
    df1.dot(df1.columns.get_level_values(1)   ',')
    .str.strip(',')
    .str.split(',',expand=True)
)
df.columns = dict.fromkeys(df1.columns.get_level_values(0)).keys()
df = df.reset_index()
 

Еще одна альтернатива-использовать mul :

 df1 = df.set_index(['event_id', 'target'])
df1.columns = df1.columns.str.split('.', expand=True, n=1)
df = (
    df1.mul(df1.columns.get_level_values(1))
    .replace('', np.NAN)
    .droplevel(1, axis =1)
    .stack()
    .unstack()
)
 

записка:

В случае, если у вас есть несколько значений для актора и агента для 1 цели, вы можете использовать pivot_table вместо stack / unstack :

 df1 = df.set_index(['event_id', 'target'])
df1.columns = df1.columns.str.split('.', expand=True, n=1)
(
    df1.mul(df1.columns.get_level_values(1))
    .replace('', np.NAN)
    .droplevel(1, axis =1)
    .stack()
    .reset_index()
    .pivot_table(index = ['event_id', 'target'], columns = 'level_2' , values = 0, aggfunc = ', '.join)
)
 

Комментарии:

1. Спасибо, это чрезвычайно полезно для новичка. Оказывается, у меня действительно были ситуации с несколькими значениями, и я работаю над методом сводной таблицы, но первый вариант помог мне понять, что происходит.

Ответ №2:

Вы можете переключить код с широкого на длинный, отфильтровать только те строки, где True есть, внести некоторые изменения, чтобы получить результат.

  # flip from wide to long
(pd.wide_to_long(df,
                 stubnames=['action','actor'], 
                 i=['event_id','target'],
                 j='subs', 
                 sep='.', 
                 suffix='. ')
  .loc[lambda df: df.any(1)] # keeps only rows that have `True`
   # if any row has `True`, 
   # replace it with values from `subs` index
  .where(lambda df: df.isna(), 
         lambda df: df.index.get_level_values('subs'))
  .fillna('')
  .droplevel('subs') # served its purpose, let it go
  # use the groupby with the agg to reduce to single rows
  .groupby(['event_id', 'target'])
  .agg("".join)
  .reset_index()
)
: 
   event_id           target   action                actor
0         1        Microsoft  malware    internal.employee
1         2  Bank of America  hacking  external.competitor