web-dev-qa-db-fra.com

Fusionner pandas dataframes où une valeur est entre deux autres

J'ai besoin de fusionner deux pandas dataframes sur un identifiant et une condition où une date dans un dataframe est entre deux dates dans l'autre dataframe.

La trame de données A a une date ("fdate") et un ID ("cusip"):

enter image description here

Je dois fusionner cela avec ce dataframe B:

enter image description here

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

En SQL, cela serait trivial, mais la seule façon dont je peux voir comment le faire dans pandas est de fusionner inconditionnellement sur l'identifiant, puis de filtrer sur la condition de 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 procéder? Il semble qu'il serait préférable de filtrer au sein de la fusion afin d'éviter d'avoir une trame de données potentiellement très volumineuse après la fusion mais avant la fin du filtre.

42
itzy

Comme vous le 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
29
ChuHo

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 de même pour vous. Je n'ai pas comparé les deux, mais c'est plus facile à lire.

15
chris dorn

Il n'y a aucun moyen pandémique de le faire pour le moment.

Cette réponse consistait à résoudre le problème du polymorphisme, ce qui s'avérait être une très mauvaise idée .

Ensuite, la fonction numpy.piecewise est apparue dans une autre réponse, mais avec peu d'explications, j'ai donc pensé clarifier comment cette fonction peut être utilisée.

Façon numpy avec par morceaux (mémoire lourde)

La fonction np.piecewise peut être utilisée pour générer le comportement d'une jointure personnalisée. Il y a beaucoup de frais généraux impliqués et ce n'est pas très efficace, mais cela fait le travail.

Produire les conditions d'adhésion

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)]

Il s'agit d'une liste de tableaux où chaque tableau nous indique si le terme intervalle de temps correspond à chacune des deux déclarations de guerre que nous avons. Les conditions peuvent exploser avec des ensembles de données plus grands car ce sera la longueur du df gauche et du df droit multiplié.

La "magie" par morceaux

Désormais, par morceaux prendra le president_id Des termes et le placera dans la trame de données war_declarations 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 terminer 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)

Je voulais partager mes efforts de recherche, donc même si cela ne résout pas le problème , j'espère que ce sera autorisé à vivre ici comme une réponse utile au moins. Comme il est difficile de repérer l'erreur, quelqu'un d'autre peut essayer cela et penser qu'il a une solution de travail, alors qu'en fait, ce n'est pas le cas.

La seule autre façon de comprendre est de créer deux nouvelles classes, une PointInTime et une Timespan

Les deux devraient avoir des méthodes __eq__ Où elles renvoient true si un PointInTime est comparé à un Timespan qui le contient.

Après cela, vous pouvez remplir votre DataFrame avec ces objets et rejoindre 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)

Remarque importante: je ne sous-classe pas datetime car pandas considérera le dtype de la colonne des objets datetime comme un dtype datetime, et comme la plage horaire n'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 DataFrames 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 jointure sur Timespan(datetime(2015,2,1), datetime(2015,4,1))

Mais ce n'est pas.

Je pense que pandas compare PointInTime(2015,3,3) à PointInTime(2015,2,2) et fait l'hypothèse que comme ils ne sont pas égaux, PointInTime(2015,3,3) ne peut pas être égal à Timespan(datetime(2015,2,1), datetime(2015,4,1)) , car cette durée était égale à PointInTime(2015,2,2)

Un peu comme ça:

Rose == Flower
Lilly != Rose

Par conséquent:

Lilly != Flower

Modifier:

J'ai essayé de rendre tous les PointInTime é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 le Timespan 2015-2-1 -> 2015-2 -5, donc cela renforce mon hypothèse ci-dessus.

Si quelqu'un a d'autres idées, veuillez commenter et je peux l'essayer.

7
firelynx

Une solution pandas serait formidable si elle était implémentée de la même manière que foverlaps () du package data.table dans R. Jusqu'à présent, j'ai trouvé que le morceau par morceaux () de numpy était efficace. J'ai fourni le code basé sur une discussion antérieure Fusion de cadres de données en fonction de la plage de dates

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
Karthik Arumugham