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"):
Je dois fusionner cela avec ce dataframe B:
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.
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
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.
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.
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.
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é.
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
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.
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)