3 votes

Détermination souple des paramètres par les pandas

Imaginons que nous ayons différentes structures de cadres de données dans Pandas.

# creating the first dataframe 
df1 = pd.DataFrame({
  "width": [1, 5], 
  "height": [5, 8]})

# creating second dataframe
df2 = pd.DataFrame({
  "a": [7, 8], 
  "b": [11, 23],
  "c": [1, 3]})

# creating second dataframe
df3 = pd.DataFrame({
  "radius": [7, 8], 
  "height": [11, 23]})

En général, il peut y avoir plus de deux cadres de données. Maintenant, je veux créer une logique qui fait correspondre les noms de colonnes à des fonctions spécifiques pour créer une nouvelle colonne "métrique" (pensez-y comme la surface pour deux colonnes et le volume pour 3 colonnes). Je veux spécifier des ensembles de noms de colonnes

column_name_ensembles = {
    "1": {
       "ensemble": ['height', 'width'],
       "method": area},
    "2": {
       "ensemble": ['a', 'b', 'c'],
       "method": volume_cube},
    "3": {
       "ensemble": ['radius', 'height'],
       "method": volume_cylinder}}

def area(width, height):
    return width * height

def volume_cube(a, b, c):
    return a * b * c

def volume_cylinder(radius, height):
    return (3.14159 * radius ** 2) * height

Maintenant, la fonction zone crée une nouvelle colonne pour le cadre de données. df1['metric'] = df1['height'] * df2['widht'] et la fonction volumen va créer une nouvelle colonne pour le dataframe df2['metic'] = df2['a'] * df2['b'] * df2['c'] . Notez que les fonctions peuvent avoir une forme arbitraire mais elles prennent l'ensemble comme paramètres. La fonction désirée metric(df, column_name_ensembles) doit prendre un cadre de données arbitraire en entrée et décider en inspectant les noms de colonnes quelle fonction doit être appliquée.

Exemple de comportement d'entrée-sortie

df1_with_metric = metric(df1, column_name_ensembles)
print(df1_with_metric)
# output
#    width height metric
#  0 1     5      5 
#  1 5     8      40
df2_with_metric = metric(df2, column_name_ensembles)
print(df2_with_metric)
# output
#    a  b  c  metric
#  0 7  11 1  77
#  1 8  23 3  552
df3_with_metric = metric(df3, column_name_ensembles)
print(df3_with_metric)
# output
#    radius  height  metric
#  0 7       11      1693.31701
#  1 8       23      4624.42048

La solution idéale serait une fonction qui prendrait le cadre de données et les noms de colonnes comme paramètres et renverrait le cadre de données avec la "métrique" appropriée ajoutée.

Je sais que cela peut être réalisé par de multiples instructions if et else, mais cela ne semble pas être la solution la plus intelligente. Il existe peut-être un modèle de conception qui peut résoudre ce problème, mais je ne suis pas un expert en modèles de conception.

Merci d'avoir lu ma question ! J'attends avec impatience vos excellentes réponses.

2voto

a_guest Points 5059

Vous pouvez utiliser le inspect pour extraire automatiquement les noms des paramètres et ensuite mettre en correspondance frozenset des noms de paramètres aux fonctions métriques directement :

import inspect

metrics = {
    frozenset(inspect.signature(f).parameters): f
    for f in (area, volume_cube, volume_cylinder)
}

Ensuite, pour un cadre de données donné, si toutes les colonnes sont garanties comme étant des arguments pour la métrique pertinente, vous pouvez simplement interroger ce dictionnaire :

def apply_metric(df, metrics):
    metric = metrics[frozenset(df.columns)]
    args = tuple(df[p] for p in inspect.signature(metric).parameters)
    df['metric'] = metric(*args)
    return df

Si le cadre de données d'entrée comporte plus de colonnes que celles requises par la fonction métrique, vous pouvez utiliser l'intersection des ensembles pour trouver la métrique appropriée :

def apply_metric(df, metrics):
    for parameters, metric in metrics.items():
        if parameters & set(df.columns) == parameters:
            args = tuple(df[p] for p in inspect.signature(metric).parameters)
            df['metric'] = metric(*args)
            break
    else:
        raise ValueError(f'No metric found for columns {df.columns}')
    return df

0voto

Chaooder Points 25
def metric(df, column_name_ensembles):

    df_cols_set = set(df.columns)
    # if there is a need to overwrite the previously calculated 'metric' column
    df_cols_set.discard('metric')

    for column_name_ensemble in column_name_ensembles.items():

        # pick up the first `column_name_ensemble` dictionary 
        # with 'ensemble' matching the df columns 
        # (excluding 'metric' column, if present)
        # comparing `set` if order of column names 
        # in ensemble does not matter (as per your df1 example), 
        # else can compare `list`
        if df_cols_set == set(column_name_ensemble[1]['ensemble']):
            df['metric'] = column_name_ensemble[1]['method'](**{col: df[col] for col in df_cols_set})
            break

    # if there is a match, return df with 'metric' calculated
    # else, return original df untouched
    return df

0voto

davidkunio Points 88

La fonction qui exécute le modèle doit être une application assez souple. En supposant que les calculs seront toujours limités aux données d'une seule ligne, cela fonctionnerait probablement.

D'abord, j'ai modifié les fonctions pour utiliser une entrée commune. J'ai ajouté un calcul de l'aire du triangle pour m'assurer que c'était extensible.

#def area(width, height):
#    return width * height

def area(row):
    return row['width'] * row['height']

#def volume_cube(a, b, c):
#    return a * b * c

def volume_cube(row):
    return row['a'] * row['b'] * row['c']

#def volume_cylinder(radius, height):
#    return (3.14159 * radius ** 2) * height

def volume_cylinder(row):
    return (3.14159 * row['radius'] ** 2) * row['height']

def area_triangle(row):
    return 0.5 * row['width'] * row['height']

Cela nous permet d'utiliser la même application pour toutes les fonctions. Parce que je suis un peu ocd, j'ai changé les noms des clés dans le dictionnaire de référence.

column_name_ensembles = {
    "area": {
       "ensemble": ['width', 'height'],
       "method": area},
    "volume_cube": {
       "ensemble": ['a', 'b', 'c'],
       "method": volume_cube},
    "volume_cylinder": {
       "ensemble": ['radius', 'height'],
       "method": volume_cylinder},
    "area_triangle": {
       "ensemble": ['width', 'height'],
       "method": area_triangle},
    }

La fonction métrique est alors une application de la df. Vous devez spécifier la fonction que vous ciblez dans cette version, mais vous pourriez déduire la méthode d'ensemble sur la base des colonnes. Cette version s'assure que les colonnes requises sont disponibles.

def metric(df,method_id):
    source_columns = list(df.columns)
    calc_columns = column_name_ensembles[method_id]['ensemble']
    if all(factor in source_columns for factor in calc_columns):
        df['metric'] = df.apply(lambda row: column_name_ensembles[method_id]['method'](row),axis=1)
        return df
    else:
        print('Column Mismatch')

Vous pouvez ensuite spécifier le cadre de données et la méthode d'ensemble.

df1_with_metric = metric(df1,'area')
df2_with_metric = metric(df2,'volume_cube')
df3_with_metric = metric(df3,'volume_cylinder')
df1_with_triangle_metric = metric(df1,'area_triangle')

0voto

villoro Points 1194

Solution

L'idée est de créer une fonction aussi générique que possible . Pour ce faire, vous devez vous appuyer sur df.apply en utilisant axis=1 pour appliquer la fonction par rangée.

La fonction serait :

def method(df, ensembles):

    # To avoid modifying the original dataframe
    df = df_in.copy()

    for data in ensembles.values():
        if set(df.columns) == set(data["ensemble"]):
            df["method"] = df.apply(lambda row: data["method"](**row), axis=1)
            return df

Pourquoi ça marche toujours ?

Il serait possible de l'appliquer même aux fonctions qui ne fonctionnent pas avec l'ensemble de la colonne.

Par exemple :

df = pd.DataFrame({
    "a": [1, 2], 
    "b": [[1, 2], [3, 4]],
})

def a_in_b(a, b):
    return a in b

# This will work
df.apply(lambda row: a_in_b(**row), axis=1)

# This won't
a_in_b(df["a"], df["b"])

0voto

anky_91 Points 26311

Voici une façon intéressante de le faire en utilisant les méthodes de pandas ( Détails ci-dessous )

def metric(dataframe,column_name_ensembles):
    func_df = pd.DataFrame(column_name_ensembles).T
    func_to_apply = func_df.loc[func_df['ensemble'].map(dataframe.columns.difference)
                        .str.len().eq(0),'method'].iat[0]
    return dataframe.assign(metric=dataframe.apply(lambda x: func_to_apply(**x),axis=1))

print(metric(df1,column_name_ensembles),'\n')
print(metric(df2,column_name_ensembles),'\n')
print(metric(df3,column_name_ensembles))

   width  height  metric
0      1       5       5
1      5       8      40 

   a   b  c  metric
0  7  11  1      77
1  8  23  3     552 

   radius  height      metric
0       7      11  1693.31701
1       8      23  4624.42048

Plus de détails :

func_df = pd.DataFrame(column_name_ensembles).T

Cela crée un cadre de données de noms de colonnes et de méthodes associées comme ci-dessous :

          ensemble                                            method
1   [height, width]             <function area at 0x000002809540F9D8>
2         [a, b, c]      <function volume_cube at 0x000002809540F950>
3  [radius, height]  <function volume_cylinder at 0x000002809540FF28>

En utilisant cette trame de données, nous trouvons la ligne où la différence entre les noms des colonnes de la trame de données transmise et la liste des colonnes de l'ensamble est de 0 en utilisant la méthode suivante pd.Index.difference , series.map , series.str.len y series.eq()

func_df['ensemble'].map(df1.columns.difference)

1                     Index([], dtype='object') <- Row matches the df columns completely
2    Index(['height', 'width'], dtype='object')
3              Index(['width'], dtype='object')
Name: ensemble, dtype: object

func_df['ensemble'].map(df1.columns.difference).str.len().eq(0)
1     True
2    False
3    False

Ensuite, lorsque c'est vrai, nous choisissons la fonction dans le champ method colonne

func_df.loc[func_df['ensemble'].map(df1.columns.difference)
                            .str.len().eq(0),'method'].iat[0]
#<function __main__.area(width, height)>

et en utilisant apply y df.assign nous créons une nouvelle ligne avec une copie du cadre de données passé.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X