Estoy trabajando en una tubería de manipulaciones en un marco de datos de pandas en clase, y me pregunto cuáles son los buenos pasos para concatenar algunos procedimientos uno tras otro: ¿debería copiar y recrear el marco de datos original, o simplemente cambiarlo en su lugar? ?
De acuerdo con la documentación de pandas, no siempre se recomienda trabajar con vistas, y no estoy seguro si este es el caso aquí.
Por ejemplo:
# 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 En la implementación anterior, obtenemos que self.df es igual a self.df_with_double_age_col e igual a self.df_with_double_age_col .
Puedes ver que si vas a ejecutar:
my_df = MyDataframe() print(my_df.df.equals(my_df.df_with_double_age_col)) # TrueNo creo que sea una buena situación (una cosa con varios nombres). Obtendré muchos alias si agrego más pasos de procesamiento. Alternativamente, me preocupaba usar solo un marco de datos y sobrescribirlo en cada paso.
Entonces, ¿qué ves como problema en esa situación? He incluido dos implementaciones opcionales adicionales para ese caso a continuación.
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.dfreturn s, tercer enfoque)): 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] Creo que la última opción es la más compacta y elegante, pero cambiar el marco de datos dado puede ser inconveniente y arriesgado (en el contexto de SettingWithCopyWarning ).
En realidad, ambas implementaciones (las tres) están mutando el df de su clase en el lugar y, por lo tanto, son prácticamente equivalentes en cuanto a su efecto en la clase. La diferencia es que en la primera y segunda implementaciones, después de mutar en el lugar, devuelve el df mutado. En la primera implementación asignándolo a diferentes atributos de clase (alias como los llame) y en la segunda implementación asignándolo a sí mismo (lo que no tiene efecto).
Entonces, si hay alguna, la tercera implementación es la más válida.
No obstante, en caso de que desee lograr una sintaxis más funcional, que se puede decir que es la forma de pandas "para concatenar algunos procedimientos uno tras otro" como indicó, puede usar funciones en lugar de métodos y pasarles el self.df como parámetro. Luego puede asignar los resultados a nuevas columnas en su self.df
Por ejemplo:
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] O en caso de que prefiera funciones que siempre devuelvan un nuevo DataFrame (para que tenga acceso a estados intermedios):
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 Preste atención a que en ambos ejemplos la función es a nivel de módulo. Por supuesto, puede optar por convertirlos en métodos, pero probablemente desee anotarlos como @staticmethod
Personalmente, no me gusta modificar los datos en su lugar. Me preocupa lo que podría suceder si se llama al método más de una vez, o lo que podría suceder si el programa falla después de algunas modificaciones en el lugar, o si otras partes del programa hacen referencia al objeto de la tabla.
Probablemente resolvería la tarea usando .pipe o .assign .
Haciendo solo pequeños cambios:
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])Dicho esto, también podrías hacer esto:
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]) )A menos que quieras ser un salvavidas, en cuyo caso:
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] }) )Si este fuera mi código base, probablemente terminaría con
df = pd.DataFrame({'name':['Alice', 'Bob', 'Carol'], 'Age':[7,15,2]}) def add_features(df:pd.DataFrame)->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)