Как в Pandas разбить одну колонку на несколько

Введение в задачу

Решил начать рассматривать нетривиальные кейсы в Pandas, с которыми иногда сталкиваюсь при работе с данными. Опять же, нетривиальные они только на мой взгляд, потому что какие-то вещи я решаю впервые и они заставляют немного подумать :) Возможно, такие небольшие кейсы помогут аналитикам, если они увидят в своих «затыках» что-то похожее. Также, я не претендую на абсолютную правильность или универсальность решения. Таким образом, у подобной задачи может быть несколько правильных решений. Я буду рад, если в комментариях вы будете предлагать свои решения. Касательно универсальности решения, тут я имею в виду, что решение может быть применимо к конкретному датасету, но при этом может не работать, если датасет будет иметь какие-то существенные видоизменения.

Первой из задач, которой я бы хотел поделиться, будет разбивка колонки датафрейма на несколько отдельных колонок с добавление к существующему датафрейму. Казалось бы просто. Давайте посмотрим решение.

Итак, у меня есть вот такой dataframe:

Скачать данные для датафрейма в csv можно тут.

Выведем первое значение из колонки new_values, чтобы лучше понять что же нам надо сделать:

Как видно, значения разделены знаком переноса строки (\n), а также каждое значение представлено в виде ключ=значение (например, ключом выступает date_start, а значением 2018-12-04).

Задача состоит в том, чтобы привести датафрейм к вот такому виду:

Решение №1

Итак, первое решение будет простым и достаточно коротким.

Сначала, нам понадобится пандосовская функция str.split (https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.str.split.html). Она просто разбивает столбец на список на основании разделителя

df['new_values'].str.split('\n')

Вот как выглядит отдельное значение:

df['new_values'].str.split('\n')[0]

Но у функции str.split есть замечательный параметр expand=True, позволяющий сразу сделать разбиение на колонки и получить датафрейм

new_df = df['new_values'].str.split('\n',expand=True)
new_df

Таким образом, мы получили дополнительный датафрейм new_df, который содержит результат разбиения. Дальше переименуем колонки датафрейма, чтобы каждая из них содержала название соответствующего ключа:

new_df.columns=['date_start','component','customer']
new_df

Затем нам нужно избавиться в колонках от названия ключа и знака «равно». Сделаем это простым циклом с функцией str.replace (https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.str.replace.html):

for column in new_df.columns:
    new_df[column] = new_df[column].str.replace(column+'=','')
new_df

Осталось только соединить два датафрейма (исходный df и new_df) с помощью функции pd.concat (https://pandas.pydata.org/pandas-docs/version/0.23.4/generated/pandas.concat.html). Обратите внимание на параметр axis=1, который позволяет соединить датафреймы по столбцам, а не по строкам

final_df = pd.concat([df,new_df],axis=1)
final_df

Ну и выкинем из получившегося датафрейма ненужный нам теперь столбец new_values с помощью drop (https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.drop.html):

final_df = final_df.drop('new_values',axis=1)
final_df

Итоговый код для решения задачи выглядит так:

df = pd.read_csv('sample_data_1.csv')
new_df = df['new_values'].str.split('\n',expand=True)
new_df.columns=['date_start','component','customer']
for column in new_df.columns:
    new_df[column] = new_df[column].str.replace(column+'=','')
final_df = pd.concat([df,new_df],axis=1).drop('new_values',axis=1)

Решение №2

Другой способ, который я покажу, является более универсальным.

Представим, что исходный датафрейм отличается от того, что я показывал выше.

Скачать данные для датафрейма в csv можно тут.

Этот датафрейм отличается тем, что в некоторых строках колонки new_values отсутствует параметр date_start. Из-за этого при попытке сделать str.split с параметром expand=True мы получим вот такой датафрейм:

df['new_values'].str.split('\n',expand=True)

В этом случае, мы не сможем просто пройтись циклом по колонкам и получить нужные значения, так как значения по ключу не всегда однозначно находятся в одной колонке, а могут быть разбросаны по нескольким. Такая же ситуация могла бы быть, если бы перечисление значений шло не в одном порядке, а по-разному. Например в одной строке date_start=2018-12-28\ncomponent=abc\ncustomer=59352, а в другой component=abc\ncustomer=22080\ndate_start=2018-12-18

Чтобы обойти эту проблему нам нужно преобразовать эти данные в такую структуру, которую было бы удобно запихнуть в датафрейм и pandas сам бы смог разделить данные по нужным колонкам, опираясь на структуру данных. Одной из таких структур может быть список словарей (list of dicts). В виде списка словарей данные должны выглядеть вот так:

Когда мы получим такую структуру, то потом сможем преобразовать её в датафрейм, а полученный датафрейм соединить с исходным (как мы уже это делали выше с помощью функции pd.concat).

Итак, первое что мы сделаем, это разобъем колонку с помощью уже знакомой функции str.split, но без параметра expand=True. Это позволит нам сделать отдельный series, содержащий списки:

s = df['new_values'].str.split('\n')
s

Затем каждый из списков нам нужно преобразовать к словарю. То есть совершить вот такое преобразование:

Чтобы лучше понять суть преобразования покажу на примере одного списка s[0], а затем сделаем функцию, которую применим к каждому элементу в series.

Сначала сделаем внутри списка s[0] вложенные списки, сделав split каждого из элемента списка по знаку «равно» через list comprehension (подробнее про list comprehension можно прочитать в статье, скажу только, что это очень удобно):

splited_items = [a.split('=') for a in s[0]]
splited_items

Затем полученный список splited_items надо переделать в словарь. Для этого сделаем несложное преобразование — создадим словарь, после чего пройдемся циклом по каждому вложенному списку и назначим нулевой элемент вложенного списка ключом словаря, а первый элемент — значением по ключу:

dictionary = {}
for item in splited_items:
    key = item[0]
    value = item[1]
    dictionary[key] = value
dictionary

Сделаем функцию, которая делает вышеописанное преобразование:

def convertToDict(x):
    splited_items = [a.split('=') for a in x]
    dictionary = {}
    for item in splited_items:
        key = item[0]
        value = item[1]
        dictionary[key] = value
    return dictionary

После этого можно применить функцию к серии s с помощью apply (https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.apply.html):

s = s.apply(lambda x: convertToDict(x))
s

Затем применим функцию pd.Series к каждому из словарей, таким образом преобразовав каждый словарь в series, а вместе последовательность series образует датафрейм:

new_df = s.apply(pd.Series)
new_df

Следующим шагом соединим датафрейм new_df с исходным df и уберем столбец с помощью функции drop:

final_df = pd.concat([df,new_df],axis=1).drop('new_values',axis=1)
final_df

Итоговый код для обработки данных выглядит вот так:

def convertToDict(x):
    splited_items = [a.split('=') for a in x]
    dictionary = {}
    for item in splited_items:
        key = item[0]
        value = item[1]
        dictionary[key] = value
    return dictionary

df = pd.read_csv('sample_data_2.csv')
s = df['new_values'].str.split('\n')
s = s.apply(lambda x: convertToDict(x))
new_df = s.apply(pd.Series)
final_df = pd.concat([df,new_df],axis=1).drop('new_values',axis=1)

И в качестве бонуса, можно сделать код более удобочитаемым с помощью method chaining:

df = pd.read_csv('sample_data_2.csv')
new_df = df['new_values'].str.split('\n').\
    apply(lambda x: convertToDict(x)).\
    apply(pd.Series)
final_df = pd.concat([df,new_df],axis=1).drop('new_values',axis=1)

Решение Романа Шапкова

UPD от 30.01.2019:
Читатель Роман решил эту задачу с помощью регулярных выражений

import pandas as pd
import re

df = pd.read_csv('sample_data_2.csv')

pattern_date = '(\d{4}-\d{2}-\d{2})'
pattern_ncomp = 'component=(\w*)'
pattern_ncust = 'customer=(\d*)'

def find_pattern(string, pattern):

    """
    аргументы: string - текстовая строка для поиска
    pattern - шаблон регулярного выражения

    функция осуществляет поиск шаблона "pattern" в строке "string" используя правила регулярных выражений(RegExp).
    Если шаблон найден - возвращает значение, иначе - возвращает None
    """
    if re.search(pattern, string):
        return re.search(pattern, string).group(1)

df['start_date'] = df['new_values'].apply(lambda x: find_pattern(x,pattern_date))
df['component'] = df['new_values'].apply(lambda x: find_pattern(x,pattern_ncomp))
df['customer'] = df['new_values'].apply(lambda x: find_pattern(x,pattern_ncust))

df

Заключение

На этом всё. Надеюсь, этот пример решения задачи кто-то найдет интересным и научится из него каким-то новым приёмам в своей работе. Это причина, по который я решил выкладывать такие примеры — не просто рассказывать про то, как работают функции в pandas или предлагать готовые решения, а показать последовательность действий и методологию решения задачи, чтобы читатели могли перенести части этого решения в свои проекты.

Если пост оказался полезным, то буду рад отзывам в комментариях, это будет для меня сигналом, что подобные вещи надо продолжать делать. А ещё лучше пишите как бы вы решили такую задачу, скидывайте примеры решений.

Вступайте в группу на Facebook и подписывайтесь на мой канал в Telegram, там публикуются интересные статьи про анализ данных и не только.

Поделиться
Отправить
Запинить
8 комментариев
Роман Шапков

я бы сделал использую RegExp(регулярные выражения).
Как по мне это гораздо проще и куда более гибче!

Пример кода ниже...
_________________________________________
import pandas as pd
import re

df = pd.read_csv(’sample_data_2.csv’)

pattern_date = ’(\d{4}-\d{2}-\d{2})’
pattern_ncomp = ’component=(\w*)’
pattern_ncust = ’customer=(\d*)’

def find_pattern(string, pattern):
«„“
аргументы: string — текстовая строка для поиска
pattern — шаблон регулярного выражения

функция осуществляет поиск шаблона „pattern“ в строке „string“ используя правила регулярных выражений(RegExp).
Если шаблон найден — возвращает значение, иначе — возвращает None
„„“
if re.search(pattern, string):
return re.search(pattern, string).group(1)

df[’start_date’] = df[’new_values’].apply(lambda x: find_pattern(x,pattern_date))
df[’component’] = df[’new_values’].apply(lambda x: find_pattern(x,pattern_ncomp))
df[’customer’] = df[’new_values’].apply(lambda x: find_pattern(x,pattern_ncust))

df

Евгений Лаптев

Я наверное чутка туповат и не понимаю следующий момент: мы читаем файл и делим колонки, но как сохранить все это в файл, чтобы после всех манипуляций сохранить в файл.

Иванов Борис

Решение задачи через apply будет более долгим по времени?

Александр Серкин

Функцию convertToDict можно убрать, используя list comprehensions, в одну строку:

new_df = df[’new_values’].str.split(’\n’).\
apply(lambda x: {item[0]: item[1] for item in [a.split(’=’) for a in x]}).\
apply(pd.Series)

Хотя, решение с регулярками от Романа тоже интересное.

Игорь

очень круто!!! спасибо искал как все сделать в np но не нашел, а тут все элементарно!!

Степан

Алексей, а Вы не знаете можно ли как-нибудь отфильтровать массив данных, который отдает Logs API метрики? Например ym:s:goalsID отдает в формате [26783777,282511884].
Есть ли какой-нибудь хороший способ, или всё придется все рано выносить в отдельные столбцы?

Ya Krab

я использовал (для своих целей ) df2 = df1[’ЗРА’].str.extract(r’(?P<Вид_ЗРА>[ВСДОКCOKB]{1,2})(?P<Номер>\d{3,4})’). с именованными группами — имена групп становятся названиями столбцов. Случайно нашел этот метод в документации пандас, так в русскоязычном интернете ни слова. Ну а вообще есть шутка: если у вас есть проблема и вы решили воспользоваться Регулярками — то теперь у вас две проблемы. Ну у меня получилось и я был в восторге от метода.

Популярное