J'essaie de reproduire, grosso modo, le paquet dplyr de R en utilisant Python/Pandas (comme exercice d'apprentissage). Quelque chose me bloque, c'est la fonctionnalité de "tuyauterie".
Dans R/dplyr, cela est fait en utilisant l'opérateur de pipe %>%
, où x %>% f(y)
est équivalent à f(x, y)
. Si possible, je voudrais reproduire ceci en utilisant la syntaxe infixe (voir ici ).
Pour illustrer, considérons les deux fonctions ci-dessous.
import pandas as pd
def select(df, *args):
cols = [x for x in args]
df = df[cols]
return df
def rename(df, **kwargs):
for name, value in kwargs.items():
df = df.rename(columns={'%s' % name: '%s' % value})
return df
La première fonction prend un cadre de données et retourne uniquement les colonnes données. La seconde prend un cadre de données et renomme les colonnes données. Par exemple:
d = {'one' : [1., 2., 3., 4., 4.],
'two' : [4., 3., 2., 1., 3.]}
df = pd.DataFrame(d)
# Keep only the 'one' column.
df = select(df, 'one')
# Rename the 'one' column to 'new_one'.
df = rename(df, one = 'new_one')
Pour obtenir la même chose en utilisant la syntaxe pipe/infix, le code serait:
df = df | select('one') \
| rename(one = 'new_one')
Ainsi, la sortie du côté gauche de |
est passée en tant que premier argument de la fonction du côté droit. Chaque fois que je vois quelque chose comme ceci fait ( ici , par exemple), cela implique des fonctions lambda. Est-il possible de canaliser le cadre de données d'un Pandas entre les fonctions de la même manière?
Je sais que Pandas utilise la méthode .pipe
, mais ce qui est important pour moi, c'est la syntaxe de l'exemple que j'ai fourni. Toute aide serait appréciée.
Il est difficile de l'implémenter à l'aide de l'opérateur or
au niveau du bit, car pandas.DataFrame
l'implémente. Si cela ne vous dérange pas de remplacer |
par >>
, vous pouvez essayer ceci:
import pandas as pd
def select(df, *args):
cols = [x for x in args]
return df[cols]
def rename(df, **kwargs):
for name, value in kwargs.items():
df = df.rename(columns={'%s' % name: '%s' % value})
return df
class SinkInto(object):
def __init__(self, function, *args, **kwargs):
self.args = args
self.kwargs = kwargs
self.function = function
def __rrshift__(self, other):
return self.function(other, *self.args, **self.kwargs)
def __repr__(self):
return "<SinkInto {} args={} kwargs={}>".format(
self.function,
self.args,
self.kwargs
)
df = pd.DataFrame({'one' : [1., 2., 3., 4., 4.],
'two' : [4., 3., 2., 1., 3.]})
Ensuite, vous pouvez faire:
>>> df
one two
0 1 4
1 2 3
2 3 2
3 4 1
4 4 3
>>> df = df >> SinkInto(select, 'one') \
>> SinkInto(rename, one='new_one')
>>> df
new_one
0 1
1 2
2 3
3 4
4 4
En Python 3, vous pouvez abuser de l'unicode:
>>> print('\u01c1')
ǁ
>>> ǁ = SinkInto
>>> df >> ǁ(select, 'one') >> ǁ(rename, one='new_one')
new_one
0 1
1 2
2 3
3 4
4 4
[mettre à jour]
Merci pour votre réponse. Serait-il possible de faire une classe séparée (comme SinkInto) pour chaque fonction pour éviter d'avoir à passer les fonctions en argument?
def pipe(original):
class PipeInto(object):
data = {'function': original}
def __init__(self, *args, **kwargs):
self.data['args'] = args
self.data['kwargs'] = kwargs
def __rrshift__(self, other):
return self.data['function'](
other,
*self.data['args'],
**self.data['kwargs']
)
return PipeInto
@pipe
def select(df, *args):
cols = [x for x in args]
return df[cols]
@pipe
def rename(df, **kwargs):
for name, value in kwargs.items():
df = df.rename(columns={'%s' % name: '%s' % value})
return df
Maintenant, vous pouvez décorer n'importe quelle fonction qui prend une DataFrame
comme premier argument:
>>> df >> select('one') >> rename(one='first')
first
0 1
1 2
2 3
3 4
4 4
Je sais que des langages comme Ruby sont "tellement expressifs" que cela encourage les gens à écrire chaque programme comme un nouvel ADSL, mais c'est un peu mal vu en Python. De nombreux pythonistes considèrent la surcharge d'opérateurs dans un but différent comme un blasphème pécheur.
L'utilisateur OHLÁLÁ n'est pas impressionné:
Le problème avec cette solution est lorsque vous essayez d'appeler la fonction au lieu de la tuyauterie. - OH LA LA
Vous pouvez implémenter la méthode dunder-call:
def __call__(self, df):
return df >> self
Et alors:
>>> select('one')(df)
one
0 1.0
1 2.0
2 3.0
3 4.0
4 4.0
On dirait que ce n'est pas facile de faire plaisir à OHLÁLÁ:
Dans ce cas, vous devez appeler l'objet explicitement:
select('one')(df)
Y a-t-il un moyen d'éviter cela? - OH LA LA
Eh bien, je peux penser à une solution, mais il y a une mise en garde: votre fonction d'origine ne doit pas prendre un deuxième argument de position qui est un cadre de données pandas (les arguments de mot clé sont acceptables). Ajoutons une méthode __new__
à notre classe PipeInto
à l'intérieur du document qui vérifie si le premier argument est un cadre de données et si c'est le cas, nous appelons simplement la fonction d'origine avec les arguments:
def __new__(cls, *args, **kwargs):
if args and isinstance(args[0], pd.DataFrame):
return cls.data['function'](*args, **kwargs)
return super().__new__(cls)
Cela semble fonctionner, mais il y a probablement des inconvénients que je n'ai pas pu identifier.
>>> select(df, 'one')
one
0 1.0
1 2.0
2 3.0
3 4.0
4 4.0
>>> df >> select('one')
one
0 1.0
1 2.0
2 3.0
3 4.0
4 4.0
Même si je ne peux pas m'empêcher de mentionner que l'utilisation de dplyr en Python est peut-être la chose la plus proche de dplyr en Python (il a l'opérateur rshift, mais en tant que gadget), je voudrais également souligner L'opérateur n'est peut-être nécessaire que dans R en raison de son utilisation de fonctions génériques plutôt que de méthodes en tant qu'attributs d'objet. La méthode chaînage vous donne essentiellement la même chose sans avoir à remplacer les opérateurs:
dataf = (DataFrame(mtcars).
filter('gear>=3').
mutate(powertoweight='hp*36/wt').
group_by('gear').
summarize(mean_ptw='mean(powertoweight)'))
Remarque Enrouler la chaîne entre une paire de parenthèses vous permet de la diviser en plusieurs lignes sans avoir besoin d'un \
final sur chaque ligne.
Vous pouvez utiliser sspipe library et utiliser la syntaxe suivante:
from sspipe import p
df = df | p(select, 'one') \
| p(rename, one = 'new_one')
Je ne pouvais pas trouver un moyen intégré de le faire, alors j'ai créé une classe qui utilise l'opérateur __call__
car elle supporte *args/**kwargs
:
class Pipe:
def __init__(self, value):
"""
Creates a new pipe with a given value.
"""
self.value = value
def __call__(self, func, *args, **kwargs):
"""
Creates a new pipe with the value returned from `func` called with
`args` and `kwargs` and it's easy to save your intermedi.
"""
value = func(self.value, *args, **kwargs)
return Pipe(value)
La syntaxe prend un certain temps pour s'y habituer, mais elle permet de créer une tuyauterie.
def get(dictionary, key):
assert isinstance(dictionary, dict)
assert isinstance(key, str)
return dictionary.get(key)
def keys(dictionary):
assert isinstance(dictionary, dict)
return dictionary.keys()
def filter_by(iterable, check):
assert hasattr(iterable, '__iter__')
assert callable(check)
return [item for item in iterable if check(item)]
def update(dictionary, **kwargs):
assert isinstance(dictionary, dict)
dictionary.update(kwargs)
return dictionary
x = Pipe({'a': 3, 'b': 4})(update, a=5, c=7, d=8, e=1)
y = (x
(keys)
(filter_by, lambda key: key in ('a', 'c', 'e', 'g'))
(set)
).value
z = x(lambda dictionary: dictionary['a']).value
assert x.value == {'a': 5, 'b': 4, 'c': 7, 'd': 8, 'e': 1}
assert y == {'a', 'c', 'e'}
assert z == 5