Fusionner les DataFrames de pandas où une valeur se situe entre deux autres [dupliquer]

cette question a déjà une réponse ici:

je dois fusionner deux DataFrames pandas sur un identifiant et une condition où une date dans une dataframe est entre deux dates dans les autres dataframe.

Dataframe a a une date ("fdate") et un ID ("cusip"):

enter image description here

je dois fusionner avec ce dataframe B:

enter image description here

sur A.cusip==B.ncusip et A.fdate se situe entre B.namedt et B.nameenddt .

en SQL ce serait trivial, mais le seul moyen que je peux voir comment faire cela en pandas doit d'abord fusionner inconditionnellement sur l'Identifiant, puis filtrer sur la condition date:

df = pd.merge(A, B, how='inner', left_on='cusip', right_on='ncusip')
df = df[(df['fdate']>=df['namedt']) & (df['fdate']<=df['nameenddt'])]

Est-ce vraiment la meilleure façon de le faire? Il semble que ce serait beaucoup mieux si l'on pouvait filtrer à l'intérieur de la fusion afin d'éviter d'avoir une base de données potentiellement très grande après la fusion mais avant que le filtre ait terminé.

33
demandé sur coldspeed 2015-06-03 21:33:39

4 réponses

comme vous dites, c'est assez facile en SQL, alors pourquoi ne pas le faire en SQL?

import pandas as pd
import sqlite3

#We'll use firelynx's tables:
presidents = pd.DataFrame({"name": ["Bush", "Obama", "Trump"],
                           "president_id":[43, 44, 45]})
terms = pd.DataFrame({'start_date': pd.date_range('2001-01-20', periods=5, freq='48M'),
                      'end_date': pd.date_range('2005-01-21', periods=5, freq='48M'),
                      'president_id': [43, 43, 44, 44, 45]})
war_declarations = pd.DataFrame({"date": [datetime(2001, 9, 14), datetime(2003, 3, 3)],
                                 "name": ["War in Afghanistan", "Iraq War"]})
#Make the db in memory
conn = sqlite3.connect(':memory:')
#write the tables
terms.to_sql('terms', conn, index=False)
presidents.to_sql('presidents', conn, index=False)
war_declarations.to_sql('wars', conn, index=False)

qry = '''
    select  
        start_date PresTermStart,
        end_date PresTermEnd,
        wars.date WarStart,
        presidents.name Pres
    from
        terms join wars on
        date between start_date and end_date join presidents on
        terms.president_id = presidents.president_id
    '''
df = pd.read_sql_query(qry, conn)

df:

         PresTermStart          PresTermEnd             WarStart  Pres
0  2001-01-31 00:00:00  2005-01-31 00:00:00  2001-09-14 00:00:00  Bush
1  2001-01-31 00:00:00  2005-01-31 00:00:00  2003-03-03 00:00:00  Bush
18
répondu ChuHo 2017-12-09 01:21:55

vous devriez pouvoir le faire maintenant en utilisant le paquet pandasql

import pandasql as ps

sqlcode = '''
select A.cusip
from A
inner join B on A.cusip=B.ncusip
where A.fdate >= B.namedt and A.fdate <= B.nameenddt
group by A.cusip
'''

newdf = ps.sqldf(sqlcode,locals())

je pense que la réponse de @ChuHo est bonne. Je crois que pandasql fait la même chose pour vous. Je n'ai pas comparé les deux, mais c'est plus facile à lire.

9
répondu chris dorn 2018-04-17 15:48:32

il n'y a pas de façon pandamique de le faire pour le moment.

cette réponse avait pour but de s'attaquer au problème du polymorphisme, qui s'est avéré être une très mauvaise idée .

puis la fonction numpy.piecewise apparaît dans une autre réponse, mais avec peu d'explications, donc j'ai pensé que je voudrais clarifier comment cette fonction peut être utilisée.

Numpy way avec piecewise (Mémoire lourde)

la fonction np.piecewise peut être utilisée pour générer le comportement d'une jointure personnalisée. Il ya beaucoup de frais généraux impliqués et il n'est pas très efficace en soi, mais il fait le travail.

conditions de production pour l'assemblage

import pandas as pd
from datetime import datetime


presidents = pd.DataFrame({"name": ["Bush", "Obama", "Trump"],
                           "president_id":[43, 44, 45]})
terms = pd.DataFrame({'start_date': pd.date_range('2001-01-20', periods=5, freq='48M'),
                      'end_date': pd.date_range('2005-01-21', periods=5, freq='48M'),
                      'president_id': [43, 43, 44, 44, 45]})
war_declarations = pd.DataFrame({"date": [datetime(2001, 9, 14), datetime(2003, 3, 3)],
                                 "name": ["War in Afghanistan", "Iraq War"]})

start_end_date_tuples = zip(terms.start_date.values, terms.end_date.values)
conditions = [(war_declarations.date.values >= start_date) &
              (war_declarations.date.values <= end_date) for start_date, end_date in start_end_date_tuples]

> conditions
[array([ True,  True], dtype=bool),
 array([False, False], dtype=bool),
 array([False, False], dtype=bool),
 array([False, False], dtype=bool),
 array([False, False], dtype=bool)]

C'est une liste de tableaux où chaque tableau nous indique si le terme laps de temps correspondant pour chacune des deux déclarations de guerre que nous avons. le les conditions peuvent exploser avec des ensembles de données plus grands comme il sera la longueur du df gauche et le DF droit multiplié.

par morceaux "magie"

maintenant par morceaux prendra le president_id des Termes et le placera dans le war_declarations dataframe pour chacune des guerres correspondantes.

war_declarations['president_id'] = np.piecewise(np.zeros(len(war_declarations)),
                                                conditions,
                                                terms.president_id.values)
    date        name                president_id
0   2001-09-14  War in Afghanistan          43.0
1   2003-03-03  Iraq War                    43.0

maintenant, pour finir cet exemple, nous avons juste besoin de fusionner régulièrement au nom des Présidents.

war_declarations.merge(presidents, on="president_id", suffixes=["_war", "_president"])

    date        name_war            president_id    name_president
0   2001-09-14  War in Afghanistan          43.0    Bush
1   2003-03-03  Iraq War                    43.0    Bush

polymorphisme (ne fonctionne pas)

j'ai voulu partager mes efforts de recherche, donc même si ce ne résout pas le problème , j'espère qu'il sera permis de vivre sur ici comme une réponse utile au moins. Comme il est difficile de repérer l'erreur, quelqu'un d'autre peut essayer et penser qu'il a une solution qui fonctionne, alors qu'en fait, il ne le fait pas.

le seul autre moyen que j'ai pu trouver est de créer deux nouvelles classes, une PointInTime et une durée

tous les deux devraient avoir des méthodes __eq__ où ils renvoient true si un Pointtintime est comparé à un temps qui le contient.

après cela, vous pouvez remplir votre DataFrame avec ces objets, et rejoindre sur les colonnes dans lesquelles ils vivent.

quelque chose comme ça:

class PointInTime(object):

    def __init__(self, year, month, day):
        self.dt = datetime(year, month, day)

    def __eq__(self, other):
        return other.start_date < self.dt < other.end_date

    def __ne__(self, other):
        return not self.__eq__(other)

    def __repr__(self):
        return "{}-{}-{}".format(self.dt.year, self.dt.month, self.dt.day)

class Timespan(object):
    def __init__(self, start_date, end_date):
        self.start_date = start_date
        self.end_date = end_date

    def __eq__(self, other):
        return self.start_date < other.dt < self.end_date

    def __ne__(self, other):
        return not self.__eq__(other)

    def __repr__(self):
        return "{}-{}-{} -> {}-{}-{}".format(self.start_date.year, self.start_date.month, self.start_date.day,
                                             self.end_date.year, self.end_date.month, self.end_date.day)

Note importante: Je ne Classe pas datetime parce que pandas considérez le dtype de la colonne des objets datetime comme un dtype datetime, et puisque le timespan ne l'est pas, pandas refuse silencieusement de fusionner sur eux.

si nous instancions deux objets de ces classes, ils peuvent maintenant être comparés:

pit = PointInTime(2015,1,1)
ts = Timespan(datetime(2014,1,1), datetime(2015,2,2))
pit == ts
True

nous pouvons également remplir deux images de données avec ces objets:

df = pd.DataFrame({"pit":[PointInTime(2015,1,1), PointInTime(2015,2,2), PointInTime(2015,3,3)]})

df2 = pd.DataFrame({"ts":[Timespan(datetime(2015,2,1), datetime(2015,2,5)), Timespan(datetime(2015,2,1), datetime(2015,4,1))]})

et puis le genre de fusion des œuvres:

pd.merge(left=df, left_on='pit', right=df2, right_on='ts')

        pit                    ts
0  2015-2-2  2015-2-1 -> 2015-2-5
1  2015-2-2  2015-2-1 -> 2015-4-1

mais seulement en quelque sorte.

PointInTime(2015,3,3) aurait également dû être inclus dans cette rubrique sur Timespan(datetime(2015,2,1), datetime(2015,4,1))

mais ce n'est pas le cas.

I figure pandas compare PointInTime(2015,3,3) à PointInTime(2015,2,2) et fait l'hypothèse que puisqu'ils ne sont pas égaux, PointInTime(2015,3,3) ne peut pas être égal à Timespan(datetime(2015,2,1), datetime(2015,4,1)) , puisque cette période était égale à PointInTime(2015,2,2)

en quelque sorte:

Rose == Flower
Lilly != Rose

donc:

Lilly != Flower

Edit:

j'ai essayé de rendre tous les Pointsintime égaux les uns aux autres, cela a changé le comportement de la jointure pour inclure le 2015-3-3, mais le 2015-2-2 n'a été inclus que pour la durée 2015-2-1 -> 2015-2-5, donc cela renforce mon hypothèse ci-dessus.

Si quelqu'un a d'autres idées, s'il vous plaît commentaire et je peux l'essayer.

6
répondu firelynx 2018-08-07 11:07:55

une solution pandas serait grande si elle était implémentée de la même manière que foverlaps() à partir de données.table package in R. jusqu'à présent, j'ai trouvé que les morceaux de numpy() sont efficaces. J'ai fourni le code basé sur une discussion antérieure fusion des dataframes basé sur la gamme de date

A['permno'] = np.piecewise(np.zeros(A.count()[0]),
                                 [ (A['cusip'].values == id) & (A['fdate'].values >= start) & (A['fdate'].values <= end) for id, start, end in zip(B['ncusip'].values, B['namedf'].values, B['nameenddt'].values)],
                                 B['permno'].values).astype(int)
3
répondu Karthik Arumugham 2017-05-23 11:55:11