На месте изменить или перезаписать фрейм данных?

#python #pandas #dataframe

#питон #панды #фрейм данных

Вопрос:

Я работаю над конвейером манипуляций с фреймом данных pandas в классе, и мне интересно, каковы хорошие шаги для объединения некоторых процедур одна за другой — должен ли я скопировать и воссоздать исходный фрейм данных или просто изменить его на месте?

Согласно документации pandas, работа с представлениями не всегда рекомендуется, и я не уверен, что здесь это так.

Например:

 # first approach import pandas as pd  class MyDataframe:  def __init__(self):  data = [['alice', 7], ['bob', 15], ['carol', 2]]  self.df = pd.DataFrame(data, columns = ['Name', 'Age'])  self.df_with_double_age_col = self._double_age()  self.df_with_double_age_col_and_first_letter = self._get_first_letter_of_name()   def _double_age(self):  self.df['double_age'] = self.df['Age'] * 2  return self.df   def _get_first_letter_of_name(self):  self.df['first_letter'] = self.df['Name'].str[0]  return self.df   

В предыдущей реализации мы получаем, что self.df равно self.df_with_double_age_col и равно self.df_with_double_age_col .

Вы можете убедиться в этом, если выполните:

 my_df = MyDataframe() print(my_df.df.equals(my_df.df_with_double_age_col)) # True  

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

Итак, в чем вы видите какую-либо проблему в этой ситуации? Я включил две дополнительные дополнительные реализации для этого случая ниже.


Перезапись (второй подход):

 import pandas as pd  class MyDataframe:  def __init__(self):  data = [['alice', 7], ['bob', 15], ['carol', 2]]  self.df = pd.DataFrame(data, columns = ['Name', 'Age'])  self.df = self._double_age()  self.df = self._get_first_letter_of_name()   def _double_age(self):  self.df['double_age'] = self.df['Age'] * 2  return self.df   def _get_first_letter_of_name(self):  self.df['first_letter'] = self.df['Name'].str[0]  return self.df  

Изменение на месте (без return s, третий подход)):

 import pandas as pd  class MyDataframe:  def __init__(self):  data = [['alice', 7], ['bob', 15], ['carol', 2]]  self.df = pd.DataFrame(data, columns = ['Name', 'Age'])  self._double_age()  self._get_first_letter_of_name()   def _double_age(self):  self.df['double_age'] = self.df['Age'] * 2    def _get_first_letter_of_name(self):  self.df['first_letter'] = self.df['Name'].str[0]  

Я считаю, что последний вариант является наиболее компактным и элегантным, но изменение данного фрейма данных может быть неудобным и рискованным (в контексте SettingWithCopyWarning ).

Ответ №1:

На самом деле, обе ваши реализации (три из них) изменяют ваш класс df на месте и, таким образом, практически эквивалентны в вопросах их влияния на класс. Разница в том, что в первой и второй реализациях после изменения на месте вы возвращаете измененное df . В первой реализации присвоение его различным атрибутам класса (псевдонимы, как вы их называете), а во второй реализации присвоение его самому себе (что не имеет никакого эффекта).

Таким образом, если таковая имеется, третья реализация является более допустимой.

Тем не менее, в случае, если вы хотите добиться более функционального синтаксиса, который, можно сказать, является pandas способом «объединения некоторых процедур одна за другой», как вы заявили, вы можете использовать функции вместо методов и передавать self.df им в качестве параметра. Затем вы можете назначить результаты новым столбцам в своем self.df .

Например:

 class MyDataframe:  def __init__(self):  data = [['alice', 7], ['bob', 15], ['carol', 2]]  self.df = pd.DataFrame(data, columns = ['Name', 'Age'])  self.df['double_age'] = _double_age(self.df)  self.df['first_letter'] = _get_first_letter_of_name(self.df)  def _double_age(df):  return df['Age'] * 2  def _get_first_letter_of_name(df):  return df['Name'].str[0]  

Или в случае, если вы предпочитаете функции, которые всегда возвращают новое DataFrame (чтобы у вас был доступ к промежуточным состояниям):

 class MyDataframe:  def __init__(self):  data = [['alice', 7], ['bob', 15], ['carol', 2]]  self.df = pd.DataFrame(data, columns = ['Name', 'Age'])  self.df_with_double_age_col = _double_age(self.df)  self.df_with_double_age_col_and_first_letter = _get_first_letter_of_name(self.df_with_double_age_col)  def _double_age(df):  new_df = df.copy()  new_df['double_age'] = new_df['Age'] * 2  return new_df  def _get_first_letter_of_name(df):  new_df = df.copy()  new_df['first_letter'] = new_df['Name'].str[0]  return new_df  

Обратите внимание, что в обоих примерах функция находится на уровне модуля. Конечно, вы можете сделать их методами, но тогда вы, вероятно, захотите прокомментировать их как @staticmethod

Ответ №2:

Лично мне не нравится изменять данные на месте. Это заставляет меня беспокоиться о том, что может произойти, если метод будет вызван более одного раза, или что может произойти, если программа выйдет из строя после некоторых изменений на месте, или если другие части программы ссылаются на объект таблицы.

Я бы, вероятно, решил эту задачу либо с помощью .pipe , либо .assign .

Внесение только небольших изменений:

 import pandas as pd  class MyDataframe:  def __init__(self):  data = [['alice', 7], ['bob', 15], ['carol', 2]]   self.df = (pd  .DataFrame(data, columns = ['Name', 'Age'])  .pipe(self._double_age)  .pipe(self._get_first_letter_of_name)  )   def _double_age(self):  return self.df.assign(double_age = lambda x: x['Age'] * 2)    def _get_first_letter_of_name(self):  return self.df.assign(first_letter = lambda x: x['Name'].str[0])  

Тем не менее, вы также могли бы сделать это:

 import pandas as pd  class MyDataframe:  def __init__(self):  data = [['alice', 7], ['bob', 15], ['carol', 2]]   self.df = (pd  .DataFrame(data, columns = ['Name', 'Age'])  .assign(double_age = lambda x: x['Age'] * 2)  .assign(first_letter = lambda x: x['Name'].str[0])  )  

Если только вы не хотите быть спасением, в этом случае:

 class MyDataframe:  def __init__(self):  data = [['alice', 7], ['bob', 15], ['carol', 2]]   self.df = (pd  .DataFrame(data, columns = ['Name', 'Age'])  .assign(**{  'double_age': lambda x: x['Age'] * 2,  'first_letter': lambda x: x['Name'].str[0]  })  )  

Если бы это была моя кодовая база, я бы, вероятно, закончил с

 df = pd.DataFrame({'name':['Alice', 'Bob', 'Carol'], 'Age':[7,15,2]})  def add_features(df:pd.DataFrame)-gt;pd.DataFrame:  ans = df.assign(**{  'double_age': lambda x: x['Age'] * 2,  'first_letter': lambda x: x['Name'].str[0]  })  return ans   df.pipe(add_features)