web-dev-qa-db-ja.com

Merge pandas 1つの値が他の2つの値の間にあるデータフレーム

2つのpandasデータフレームを識別子と1つのデータフレームの日付が他のデータフレームの2つの日付の間にあるという条件でデータフレームをマージする必要があります。

データフレームAには日付(「fdate」)とID(「cusip」)があります。

enter image description here

これをこのデータフレームBとマージする必要があります。

enter image description here

A.cusip==B.ncusipおよびA.fdate は間に B.namedtおよびB.nameenddt

SQLではこれは簡単ですが、pandasでこれを行う方法を見ることができる唯一の方法は、最初に識別子で無条件にマージし、次に日付条件でフィルタリングすることです:

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

これは本当にこれを行うための最良の方法ですか?マージの後、フィルターが完了する前に潜在的に非常に大きなデータフレームを持たないように、マージ内でフィルターをかけることができれば、はるかに良いと思われます。

42
itzy

あなたが言うように、これはSQLでは非常に簡単ですので、なぜ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

パッケージ 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())

@ChuHoからの答えは良いと思います。私はpandasqlがあなたのために同じことをしていると信じています。この2つのベンチマークは行っていませんが、読みやすくなっています。

15
chris dorn

今のところこれを行うパンダミックな方法はありません。

この答えは、非常に悪いアイデアであることが判明した多型の問題に取り組むことについてでした。

その後、別の答えに numpy.piecewise 関数が登場しましたが、説明はほとんどありませんでしたので、この関数の使用方法を明確にすると思いました。

区分的でナンピーな方法(メモリが重い)

np.piecewise 関数を使用して、カスタム結合の動作を生成できます。多くのオーバーヘッドが関係しており、あまり効率的ではありませんが、仕事はします。

参加条件の作成

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

これは配列のリストであり、各配列は、2つの戦争宣言のそれぞれについて期間が一致するかどうかを示します。 左のdfと右のdfの長さが乗算されるため、条件はより大きなデータセットで爆発する可能性があります。

区分的な「魔法」

これで、区分的にpresident_idを用語から取得し、対応する各戦争のwar_declarationsデータフレームに配置します。

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

この例を完了するには、大統領の名前を定期的にマージするだけです。

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

ポリモーフィズム(機能しません)

私の研究努力を共有したかったので、たとえこれが問題を解決しなくても、そうなることを望みます少なくとも有用な返信としてここに住むことを許可されました。エラーを見つけるのは難しいので、他の誰かがこれを試して、実際に解決策があると思うかもしれませんが、実際にはそうではありません。

私が理解できる他の唯一の方法は、PointInTimeとTimespanの2つの新しいクラスを作成することです

両方に__eq__メソッドがあり、PointInTimeが含まれるTimespanと比較される場合にtrueを返す必要があります。

その後、これらのオブジェクトをDataFrameに入力し、それらが存在する列に結合できます。

このようなもの:

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)

重要な注意:pandasはdatetimeオブジェクトの列のdtypeをdatetime dtypeと見なし、タイムスパンがそうではないため、pandasは静かにマージを拒否するため、datetimeをサブクラス化しません。それらの上に。

これらのクラスの2つのオブジェクトをインスタンス化すると、それらを比較できるようになります。

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

これらのオブジェクトで2つのDataFrameを埋めることもできます。

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

そして、マージの種類の作品:

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

しかし、唯一の種類。

PointInTime(2015,3,3)Timespan(datetime(2015,2,1), datetime(2015,4,1))のこの結合に含まれているはずです。

そうではありません。

pandasはPointInTime(2015,3,3)PointInTime(2015,2,2)を比較し、それらが等しくないため、PointInTime(2015,3,3)Timespan(datetime(2015,2,1), datetime(2015,4,1))と等しくできないと仮定しています、このタイムスパンはPointInTime(2015,2,2)と等しいため

このような並べ替え:

Rose == Flower
Lilly != Rose

したがって:

Lilly != Flower

編集:

私はすべてのPointInTimeを互いに等しくしようとしましたが、これにより結合の動作が2015-3-3を含むように変更されましたが、2015-2-2はTimespan 2015-2-1-> 2015-2にのみ含まれていました-5、したがって、これは私の上記の仮説を強化します。

誰か他のアイデアがあれば、コメントしてください、私はそれを試すことができます。

7
firelynx

A pandasソリューションは、Rのdata.tableパッケージのfoverlaps()と同様に実装されると素晴らしいでしょう。これまでのところ、numpyのpiecewise()が効率的であることがわかりました。以前の議論に基づく 日付範囲に基づくデータフレームの結合

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