test_merge_asof.py 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038
  1. import numpy as np
  2. import pytest
  3. import pytz
  4. import pandas as pd
  5. from pandas import Timedelta, merge_asof, read_csv, to_datetime
  6. from pandas.core.reshape.merge import MergeError
  7. from pandas.util.testing import assert_frame_equal
  8. class TestAsOfMerge(object):
  9. def read_data(self, datapath, name, dedupe=False):
  10. path = datapath('reshape', 'merge', 'data', name)
  11. x = read_csv(path)
  12. if dedupe:
  13. x = (x.drop_duplicates(['time', 'ticker'], keep='last')
  14. .reset_index(drop=True)
  15. )
  16. x.time = to_datetime(x.time)
  17. return x
  18. @pytest.fixture(autouse=True)
  19. def setup_method(self, datapath):
  20. self.trades = self.read_data(datapath, 'trades.csv')
  21. self.quotes = self.read_data(datapath, 'quotes.csv', dedupe=True)
  22. self.asof = self.read_data(datapath, 'asof.csv')
  23. self.tolerance = self.read_data(datapath, 'tolerance.csv')
  24. self.allow_exact_matches = self.read_data(datapath,
  25. 'allow_exact_matches.csv')
  26. self.allow_exact_matches_and_tolerance = self.read_data(
  27. datapath, 'allow_exact_matches_and_tolerance.csv')
  28. def test_examples1(self):
  29. """ doc-string examples """
  30. left = pd.DataFrame({'a': [1, 5, 10],
  31. 'left_val': ['a', 'b', 'c']})
  32. right = pd.DataFrame({'a': [1, 2, 3, 6, 7],
  33. 'right_val': [1, 2, 3, 6, 7]})
  34. expected = pd.DataFrame({'a': [1, 5, 10],
  35. 'left_val': ['a', 'b', 'c'],
  36. 'right_val': [1, 3, 7]})
  37. result = pd.merge_asof(left, right, on='a')
  38. assert_frame_equal(result, expected)
  39. def test_examples2(self):
  40. """ doc-string examples """
  41. trades = pd.DataFrame({
  42. 'time': pd.to_datetime(['20160525 13:30:00.023',
  43. '20160525 13:30:00.038',
  44. '20160525 13:30:00.048',
  45. '20160525 13:30:00.048',
  46. '20160525 13:30:00.048']),
  47. 'ticker': ['MSFT', 'MSFT',
  48. 'GOOG', 'GOOG', 'AAPL'],
  49. 'price': [51.95, 51.95,
  50. 720.77, 720.92, 98.00],
  51. 'quantity': [75, 155,
  52. 100, 100, 100]},
  53. columns=['time', 'ticker', 'price', 'quantity'])
  54. quotes = pd.DataFrame({
  55. 'time': pd.to_datetime(['20160525 13:30:00.023',
  56. '20160525 13:30:00.023',
  57. '20160525 13:30:00.030',
  58. '20160525 13:30:00.041',
  59. '20160525 13:30:00.048',
  60. '20160525 13:30:00.049',
  61. '20160525 13:30:00.072',
  62. '20160525 13:30:00.075']),
  63. 'ticker': ['GOOG', 'MSFT', 'MSFT',
  64. 'MSFT', 'GOOG', 'AAPL', 'GOOG',
  65. 'MSFT'],
  66. 'bid': [720.50, 51.95, 51.97, 51.99,
  67. 720.50, 97.99, 720.50, 52.01],
  68. 'ask': [720.93, 51.96, 51.98, 52.00,
  69. 720.93, 98.01, 720.88, 52.03]},
  70. columns=['time', 'ticker', 'bid', 'ask'])
  71. pd.merge_asof(trades, quotes,
  72. on='time',
  73. by='ticker')
  74. pd.merge_asof(trades, quotes,
  75. on='time',
  76. by='ticker',
  77. tolerance=pd.Timedelta('2ms'))
  78. expected = pd.DataFrame({
  79. 'time': pd.to_datetime(['20160525 13:30:00.023',
  80. '20160525 13:30:00.038',
  81. '20160525 13:30:00.048',
  82. '20160525 13:30:00.048',
  83. '20160525 13:30:00.048']),
  84. 'ticker': ['MSFT', 'MSFT', 'GOOG', 'GOOG', 'AAPL'],
  85. 'price': [51.95, 51.95,
  86. 720.77, 720.92, 98.00],
  87. 'quantity': [75, 155,
  88. 100, 100, 100],
  89. 'bid': [np.nan, 51.97, np.nan,
  90. np.nan, np.nan],
  91. 'ask': [np.nan, 51.98, np.nan,
  92. np.nan, np.nan]},
  93. columns=['time', 'ticker', 'price', 'quantity',
  94. 'bid', 'ask'])
  95. result = pd.merge_asof(trades, quotes,
  96. on='time',
  97. by='ticker',
  98. tolerance=pd.Timedelta('10ms'),
  99. allow_exact_matches=False)
  100. assert_frame_equal(result, expected)
  101. def test_examples3(self):
  102. """ doc-string examples """
  103. # GH14887
  104. left = pd.DataFrame({'a': [1, 5, 10],
  105. 'left_val': ['a', 'b', 'c']})
  106. right = pd.DataFrame({'a': [1, 2, 3, 6, 7],
  107. 'right_val': [1, 2, 3, 6, 7]})
  108. expected = pd.DataFrame({'a': [1, 5, 10],
  109. 'left_val': ['a', 'b', 'c'],
  110. 'right_val': [1, 6, np.nan]})
  111. result = pd.merge_asof(left, right, on='a', direction='forward')
  112. assert_frame_equal(result, expected)
  113. def test_examples4(self):
  114. """ doc-string examples """
  115. # GH14887
  116. left = pd.DataFrame({'a': [1, 5, 10],
  117. 'left_val': ['a', 'b', 'c']})
  118. right = pd.DataFrame({'a': [1, 2, 3, 6, 7],
  119. 'right_val': [1, 2, 3, 6, 7]})
  120. expected = pd.DataFrame({'a': [1, 5, 10],
  121. 'left_val': ['a', 'b', 'c'],
  122. 'right_val': [1, 6, 7]})
  123. result = pd.merge_asof(left, right, on='a', direction='nearest')
  124. assert_frame_equal(result, expected)
  125. def test_basic(self):
  126. expected = self.asof
  127. trades = self.trades
  128. quotes = self.quotes
  129. result = merge_asof(trades, quotes,
  130. on='time',
  131. by='ticker')
  132. assert_frame_equal(result, expected)
  133. def test_basic_categorical(self):
  134. expected = self.asof
  135. trades = self.trades.copy()
  136. trades.ticker = trades.ticker.astype('category')
  137. quotes = self.quotes.copy()
  138. quotes.ticker = quotes.ticker.astype('category')
  139. expected.ticker = expected.ticker.astype('category')
  140. result = merge_asof(trades, quotes,
  141. on='time',
  142. by='ticker')
  143. assert_frame_equal(result, expected)
  144. def test_basic_left_index(self):
  145. # GH14253
  146. expected = self.asof
  147. trades = self.trades.set_index('time')
  148. quotes = self.quotes
  149. result = merge_asof(trades, quotes,
  150. left_index=True,
  151. right_on='time',
  152. by='ticker')
  153. # left-only index uses right's index, oddly
  154. expected.index = result.index
  155. # time column appears after left's columns
  156. expected = expected[result.columns]
  157. assert_frame_equal(result, expected)
  158. def test_basic_right_index(self):
  159. expected = self.asof
  160. trades = self.trades
  161. quotes = self.quotes.set_index('time')
  162. result = merge_asof(trades, quotes,
  163. left_on='time',
  164. right_index=True,
  165. by='ticker')
  166. assert_frame_equal(result, expected)
  167. def test_basic_left_index_right_index(self):
  168. expected = self.asof.set_index('time')
  169. trades = self.trades.set_index('time')
  170. quotes = self.quotes.set_index('time')
  171. result = merge_asof(trades, quotes,
  172. left_index=True,
  173. right_index=True,
  174. by='ticker')
  175. assert_frame_equal(result, expected)
  176. def test_multi_index(self):
  177. # MultiIndex is prohibited
  178. trades = self.trades.set_index(['time', 'price'])
  179. quotes = self.quotes.set_index('time')
  180. with pytest.raises(MergeError):
  181. merge_asof(trades, quotes,
  182. left_index=True,
  183. right_index=True)
  184. trades = self.trades.set_index('time')
  185. quotes = self.quotes.set_index(['time', 'bid'])
  186. with pytest.raises(MergeError):
  187. merge_asof(trades, quotes,
  188. left_index=True,
  189. right_index=True)
  190. def test_on_and_index(self):
  191. # 'on' parameter and index together is prohibited
  192. trades = self.trades.set_index('time')
  193. quotes = self.quotes.set_index('time')
  194. with pytest.raises(MergeError):
  195. merge_asof(trades, quotes,
  196. left_on='price',
  197. left_index=True,
  198. right_index=True)
  199. trades = self.trades.set_index('time')
  200. quotes = self.quotes.set_index('time')
  201. with pytest.raises(MergeError):
  202. merge_asof(trades, quotes,
  203. right_on='bid',
  204. left_index=True,
  205. right_index=True)
  206. def test_basic_left_by_right_by(self):
  207. # GH14253
  208. expected = self.asof
  209. trades = self.trades
  210. quotes = self.quotes
  211. result = merge_asof(trades, quotes,
  212. on='time',
  213. left_by='ticker',
  214. right_by='ticker')
  215. assert_frame_equal(result, expected)
  216. def test_missing_right_by(self):
  217. expected = self.asof
  218. trades = self.trades
  219. quotes = self.quotes
  220. q = quotes[quotes.ticker != 'MSFT']
  221. result = merge_asof(trades, q,
  222. on='time',
  223. by='ticker')
  224. expected.loc[expected.ticker == 'MSFT', ['bid', 'ask']] = np.nan
  225. assert_frame_equal(result, expected)
  226. def test_multiby(self):
  227. # GH13936
  228. trades = pd.DataFrame({
  229. 'time': pd.to_datetime(['20160525 13:30:00.023',
  230. '20160525 13:30:00.023',
  231. '20160525 13:30:00.046',
  232. '20160525 13:30:00.048',
  233. '20160525 13:30:00.050']),
  234. 'ticker': ['MSFT', 'MSFT',
  235. 'GOOG', 'GOOG', 'AAPL'],
  236. 'exch': ['ARCA', 'NSDQ', 'NSDQ', 'BATS', 'NSDQ'],
  237. 'price': [51.95, 51.95,
  238. 720.77, 720.92, 98.00],
  239. 'quantity': [75, 155,
  240. 100, 100, 100]},
  241. columns=['time', 'ticker', 'exch',
  242. 'price', 'quantity'])
  243. quotes = pd.DataFrame({
  244. 'time': pd.to_datetime(['20160525 13:30:00.023',
  245. '20160525 13:30:00.023',
  246. '20160525 13:30:00.030',
  247. '20160525 13:30:00.041',
  248. '20160525 13:30:00.045',
  249. '20160525 13:30:00.049']),
  250. 'ticker': ['GOOG', 'MSFT', 'MSFT',
  251. 'MSFT', 'GOOG', 'AAPL'],
  252. 'exch': ['BATS', 'NSDQ', 'ARCA', 'ARCA',
  253. 'NSDQ', 'ARCA'],
  254. 'bid': [720.51, 51.95, 51.97, 51.99,
  255. 720.50, 97.99],
  256. 'ask': [720.92, 51.96, 51.98, 52.00,
  257. 720.93, 98.01]},
  258. columns=['time', 'ticker', 'exch', 'bid', 'ask'])
  259. expected = pd.DataFrame({
  260. 'time': pd.to_datetime(['20160525 13:30:00.023',
  261. '20160525 13:30:00.023',
  262. '20160525 13:30:00.046',
  263. '20160525 13:30:00.048',
  264. '20160525 13:30:00.050']),
  265. 'ticker': ['MSFT', 'MSFT',
  266. 'GOOG', 'GOOG', 'AAPL'],
  267. 'exch': ['ARCA', 'NSDQ', 'NSDQ', 'BATS', 'NSDQ'],
  268. 'price': [51.95, 51.95,
  269. 720.77, 720.92, 98.00],
  270. 'quantity': [75, 155,
  271. 100, 100, 100],
  272. 'bid': [np.nan, 51.95, 720.50, 720.51, np.nan],
  273. 'ask': [np.nan, 51.96, 720.93, 720.92, np.nan]},
  274. columns=['time', 'ticker', 'exch',
  275. 'price', 'quantity', 'bid', 'ask'])
  276. result = pd.merge_asof(trades, quotes, on='time',
  277. by=['ticker', 'exch'])
  278. assert_frame_equal(result, expected)
  279. def test_multiby_heterogeneous_types(self):
  280. # GH13936
  281. trades = pd.DataFrame({
  282. 'time': pd.to_datetime(['20160525 13:30:00.023',
  283. '20160525 13:30:00.023',
  284. '20160525 13:30:00.046',
  285. '20160525 13:30:00.048',
  286. '20160525 13:30:00.050']),
  287. 'ticker': [0, 0, 1, 1, 2],
  288. 'exch': ['ARCA', 'NSDQ', 'NSDQ', 'BATS', 'NSDQ'],
  289. 'price': [51.95, 51.95,
  290. 720.77, 720.92, 98.00],
  291. 'quantity': [75, 155,
  292. 100, 100, 100]},
  293. columns=['time', 'ticker', 'exch',
  294. 'price', 'quantity'])
  295. quotes = pd.DataFrame({
  296. 'time': pd.to_datetime(['20160525 13:30:00.023',
  297. '20160525 13:30:00.023',
  298. '20160525 13:30:00.030',
  299. '20160525 13:30:00.041',
  300. '20160525 13:30:00.045',
  301. '20160525 13:30:00.049']),
  302. 'ticker': [1, 0, 0, 0, 1, 2],
  303. 'exch': ['BATS', 'NSDQ', 'ARCA', 'ARCA',
  304. 'NSDQ', 'ARCA'],
  305. 'bid': [720.51, 51.95, 51.97, 51.99,
  306. 720.50, 97.99],
  307. 'ask': [720.92, 51.96, 51.98, 52.00,
  308. 720.93, 98.01]},
  309. columns=['time', 'ticker', 'exch', 'bid', 'ask'])
  310. expected = pd.DataFrame({
  311. 'time': pd.to_datetime(['20160525 13:30:00.023',
  312. '20160525 13:30:00.023',
  313. '20160525 13:30:00.046',
  314. '20160525 13:30:00.048',
  315. '20160525 13:30:00.050']),
  316. 'ticker': [0, 0, 1, 1, 2],
  317. 'exch': ['ARCA', 'NSDQ', 'NSDQ', 'BATS', 'NSDQ'],
  318. 'price': [51.95, 51.95,
  319. 720.77, 720.92, 98.00],
  320. 'quantity': [75, 155,
  321. 100, 100, 100],
  322. 'bid': [np.nan, 51.95, 720.50, 720.51, np.nan],
  323. 'ask': [np.nan, 51.96, 720.93, 720.92, np.nan]},
  324. columns=['time', 'ticker', 'exch',
  325. 'price', 'quantity', 'bid', 'ask'])
  326. result = pd.merge_asof(trades, quotes, on='time',
  327. by=['ticker', 'exch'])
  328. assert_frame_equal(result, expected)
  329. def test_multiby_indexed(self):
  330. # GH15676
  331. left = pd.DataFrame([
  332. [pd.to_datetime('20160602'), 1, 'a'],
  333. [pd.to_datetime('20160602'), 2, 'a'],
  334. [pd.to_datetime('20160603'), 1, 'b'],
  335. [pd.to_datetime('20160603'), 2, 'b']],
  336. columns=['time', 'k1', 'k2']).set_index('time')
  337. right = pd.DataFrame([
  338. [pd.to_datetime('20160502'), 1, 'a', 1.0],
  339. [pd.to_datetime('20160502'), 2, 'a', 2.0],
  340. [pd.to_datetime('20160503'), 1, 'b', 3.0],
  341. [pd.to_datetime('20160503'), 2, 'b', 4.0]],
  342. columns=['time', 'k1', 'k2', 'value']).set_index('time')
  343. expected = pd.DataFrame([
  344. [pd.to_datetime('20160602'), 1, 'a', 1.0],
  345. [pd.to_datetime('20160602'), 2, 'a', 2.0],
  346. [pd.to_datetime('20160603'), 1, 'b', 3.0],
  347. [pd.to_datetime('20160603'), 2, 'b', 4.0]],
  348. columns=['time', 'k1', 'k2', 'value']).set_index('time')
  349. result = pd.merge_asof(left,
  350. right,
  351. left_index=True,
  352. right_index=True,
  353. by=['k1', 'k2'])
  354. assert_frame_equal(expected, result)
  355. with pytest.raises(MergeError):
  356. pd.merge_asof(left, right, left_index=True, right_index=True,
  357. left_by=['k1', 'k2'], right_by=['k1'])
  358. def test_basic2(self, datapath):
  359. expected = self.read_data(datapath, 'asof2.csv')
  360. trades = self.read_data(datapath, 'trades2.csv')
  361. quotes = self.read_data(datapath, 'quotes2.csv', dedupe=True)
  362. result = merge_asof(trades, quotes,
  363. on='time',
  364. by='ticker')
  365. assert_frame_equal(result, expected)
  366. def test_basic_no_by(self):
  367. f = lambda x: x[x.ticker == 'MSFT'].drop('ticker', axis=1) \
  368. .reset_index(drop=True)
  369. # just use a single ticker
  370. expected = f(self.asof)
  371. trades = f(self.trades)
  372. quotes = f(self.quotes)
  373. result = merge_asof(trades, quotes,
  374. on='time')
  375. assert_frame_equal(result, expected)
  376. def test_valid_join_keys(self):
  377. trades = self.trades
  378. quotes = self.quotes
  379. with pytest.raises(MergeError):
  380. merge_asof(trades, quotes,
  381. left_on='time',
  382. right_on='bid',
  383. by='ticker')
  384. with pytest.raises(MergeError):
  385. merge_asof(trades, quotes,
  386. on=['time', 'ticker'],
  387. by='ticker')
  388. with pytest.raises(MergeError):
  389. merge_asof(trades, quotes,
  390. by='ticker')
  391. def test_with_duplicates(self, datapath):
  392. q = pd.concat([self.quotes, self.quotes]).sort_values(
  393. ['time', 'ticker']).reset_index(drop=True)
  394. result = merge_asof(self.trades, q,
  395. on='time',
  396. by='ticker')
  397. expected = self.read_data(datapath, 'asof.csv')
  398. assert_frame_equal(result, expected)
  399. def test_with_duplicates_no_on(self):
  400. df1 = pd.DataFrame({'key': [1, 1, 3],
  401. 'left_val': [1, 2, 3]})
  402. df2 = pd.DataFrame({'key': [1, 2, 2],
  403. 'right_val': [1, 2, 3]})
  404. result = merge_asof(df1, df2, on='key')
  405. expected = pd.DataFrame({'key': [1, 1, 3],
  406. 'left_val': [1, 2, 3],
  407. 'right_val': [1, 1, 3]})
  408. assert_frame_equal(result, expected)
  409. def test_valid_allow_exact_matches(self):
  410. trades = self.trades
  411. quotes = self.quotes
  412. with pytest.raises(MergeError):
  413. merge_asof(trades, quotes,
  414. on='time',
  415. by='ticker',
  416. allow_exact_matches='foo')
  417. def test_valid_tolerance(self):
  418. trades = self.trades
  419. quotes = self.quotes
  420. # dti
  421. merge_asof(trades, quotes,
  422. on='time',
  423. by='ticker',
  424. tolerance=Timedelta('1s'))
  425. # integer
  426. merge_asof(trades.reset_index(), quotes.reset_index(),
  427. on='index',
  428. by='ticker',
  429. tolerance=1)
  430. # incompat
  431. with pytest.raises(MergeError):
  432. merge_asof(trades, quotes,
  433. on='time',
  434. by='ticker',
  435. tolerance=1)
  436. # invalid
  437. with pytest.raises(MergeError):
  438. merge_asof(trades.reset_index(), quotes.reset_index(),
  439. on='index',
  440. by='ticker',
  441. tolerance=1.0)
  442. # invalid negative
  443. with pytest.raises(MergeError):
  444. merge_asof(trades, quotes,
  445. on='time',
  446. by='ticker',
  447. tolerance=-Timedelta('1s'))
  448. with pytest.raises(MergeError):
  449. merge_asof(trades.reset_index(), quotes.reset_index(),
  450. on='index',
  451. by='ticker',
  452. tolerance=-1)
  453. def test_non_sorted(self):
  454. trades = self.trades.sort_values('time', ascending=False)
  455. quotes = self.quotes.sort_values('time', ascending=False)
  456. # we require that we are already sorted on time & quotes
  457. assert not trades.time.is_monotonic
  458. assert not quotes.time.is_monotonic
  459. with pytest.raises(ValueError):
  460. merge_asof(trades, quotes,
  461. on='time',
  462. by='ticker')
  463. trades = self.trades.sort_values('time')
  464. assert trades.time.is_monotonic
  465. assert not quotes.time.is_monotonic
  466. with pytest.raises(ValueError):
  467. merge_asof(trades, quotes,
  468. on='time',
  469. by='ticker')
  470. quotes = self.quotes.sort_values('time')
  471. assert trades.time.is_monotonic
  472. assert quotes.time.is_monotonic
  473. # ok, though has dupes
  474. merge_asof(trades, self.quotes,
  475. on='time',
  476. by='ticker')
  477. def test_tolerance(self):
  478. trades = self.trades
  479. quotes = self.quotes
  480. result = merge_asof(trades, quotes,
  481. on='time',
  482. by='ticker',
  483. tolerance=Timedelta('1day'))
  484. expected = self.tolerance
  485. assert_frame_equal(result, expected)
  486. def test_tolerance_forward(self):
  487. # GH14887
  488. left = pd.DataFrame({'a': [1, 5, 10],
  489. 'left_val': ['a', 'b', 'c']})
  490. right = pd.DataFrame({'a': [1, 2, 3, 7, 11],
  491. 'right_val': [1, 2, 3, 7, 11]})
  492. expected = pd.DataFrame({'a': [1, 5, 10],
  493. 'left_val': ['a', 'b', 'c'],
  494. 'right_val': [1, np.nan, 11]})
  495. result = pd.merge_asof(left, right, on='a', direction='forward',
  496. tolerance=1)
  497. assert_frame_equal(result, expected)
  498. def test_tolerance_nearest(self):
  499. # GH14887
  500. left = pd.DataFrame({'a': [1, 5, 10],
  501. 'left_val': ['a', 'b', 'c']})
  502. right = pd.DataFrame({'a': [1, 2, 3, 7, 11],
  503. 'right_val': [1, 2, 3, 7, 11]})
  504. expected = pd.DataFrame({'a': [1, 5, 10],
  505. 'left_val': ['a', 'b', 'c'],
  506. 'right_val': [1, np.nan, 11]})
  507. result = pd.merge_asof(left, right, on='a', direction='nearest',
  508. tolerance=1)
  509. assert_frame_equal(result, expected)
  510. def test_tolerance_tz(self):
  511. # GH 14844
  512. left = pd.DataFrame(
  513. {'date': pd.date_range(start=pd.to_datetime('2016-01-02'),
  514. freq='D', periods=5,
  515. tz=pytz.timezone('UTC')),
  516. 'value1': np.arange(5)})
  517. right = pd.DataFrame(
  518. {'date': pd.date_range(start=pd.to_datetime('2016-01-01'),
  519. freq='D', periods=5,
  520. tz=pytz.timezone('UTC')),
  521. 'value2': list("ABCDE")})
  522. result = pd.merge_asof(left, right, on='date',
  523. tolerance=pd.Timedelta('1 day'))
  524. expected = pd.DataFrame(
  525. {'date': pd.date_range(start=pd.to_datetime('2016-01-02'),
  526. freq='D', periods=5,
  527. tz=pytz.timezone('UTC')),
  528. 'value1': np.arange(5),
  529. 'value2': list("BCDEE")})
  530. assert_frame_equal(result, expected)
  531. def test_tolerance_float(self):
  532. # GH22981
  533. left = pd.DataFrame({'a': [1.1, 3.5, 10.9],
  534. 'left_val': ['a', 'b', 'c']})
  535. right = pd.DataFrame({'a': [1.0, 2.5, 3.3, 7.5, 11.5],
  536. 'right_val': [1.0, 2.5, 3.3, 7.5, 11.5]})
  537. expected = pd.DataFrame({'a': [1.1, 3.5, 10.9],
  538. 'left_val': ['a', 'b', 'c'],
  539. 'right_val': [1, 3.3, np.nan]})
  540. result = pd.merge_asof(left, right, on='a', direction='nearest',
  541. tolerance=0.5)
  542. assert_frame_equal(result, expected)
  543. def test_index_tolerance(self):
  544. # GH 15135
  545. expected = self.tolerance.set_index('time')
  546. trades = self.trades.set_index('time')
  547. quotes = self.quotes.set_index('time')
  548. result = pd.merge_asof(trades, quotes,
  549. left_index=True,
  550. right_index=True,
  551. by='ticker',
  552. tolerance=pd.Timedelta('1day'))
  553. assert_frame_equal(result, expected)
  554. def test_allow_exact_matches(self):
  555. result = merge_asof(self.trades, self.quotes,
  556. on='time',
  557. by='ticker',
  558. allow_exact_matches=False)
  559. expected = self.allow_exact_matches
  560. assert_frame_equal(result, expected)
  561. def test_allow_exact_matches_forward(self):
  562. # GH14887
  563. left = pd.DataFrame({'a': [1, 5, 10],
  564. 'left_val': ['a', 'b', 'c']})
  565. right = pd.DataFrame({'a': [1, 2, 3, 7, 11],
  566. 'right_val': [1, 2, 3, 7, 11]})
  567. expected = pd.DataFrame({'a': [1, 5, 10],
  568. 'left_val': ['a', 'b', 'c'],
  569. 'right_val': [2, 7, 11]})
  570. result = pd.merge_asof(left, right, on='a', direction='forward',
  571. allow_exact_matches=False)
  572. assert_frame_equal(result, expected)
  573. def test_allow_exact_matches_nearest(self):
  574. # GH14887
  575. left = pd.DataFrame({'a': [1, 5, 10],
  576. 'left_val': ['a', 'b', 'c']})
  577. right = pd.DataFrame({'a': [1, 2, 3, 7, 11],
  578. 'right_val': [1, 2, 3, 7, 11]})
  579. expected = pd.DataFrame({'a': [1, 5, 10],
  580. 'left_val': ['a', 'b', 'c'],
  581. 'right_val': [2, 3, 11]})
  582. result = pd.merge_asof(left, right, on='a', direction='nearest',
  583. allow_exact_matches=False)
  584. assert_frame_equal(result, expected)
  585. def test_allow_exact_matches_and_tolerance(self):
  586. result = merge_asof(self.trades, self.quotes,
  587. on='time',
  588. by='ticker',
  589. tolerance=Timedelta('100ms'),
  590. allow_exact_matches=False)
  591. expected = self.allow_exact_matches_and_tolerance
  592. assert_frame_equal(result, expected)
  593. def test_allow_exact_matches_and_tolerance2(self):
  594. # GH 13695
  595. df1 = pd.DataFrame({
  596. 'time': pd.to_datetime(['2016-07-15 13:30:00.030']),
  597. 'username': ['bob']})
  598. df2 = pd.DataFrame({
  599. 'time': pd.to_datetime(['2016-07-15 13:30:00.000',
  600. '2016-07-15 13:30:00.030']),
  601. 'version': [1, 2]})
  602. result = pd.merge_asof(df1, df2, on='time')
  603. expected = pd.DataFrame({
  604. 'time': pd.to_datetime(['2016-07-15 13:30:00.030']),
  605. 'username': ['bob'],
  606. 'version': [2]})
  607. assert_frame_equal(result, expected)
  608. result = pd.merge_asof(df1, df2, on='time', allow_exact_matches=False)
  609. expected = pd.DataFrame({
  610. 'time': pd.to_datetime(['2016-07-15 13:30:00.030']),
  611. 'username': ['bob'],
  612. 'version': [1]})
  613. assert_frame_equal(result, expected)
  614. result = pd.merge_asof(df1, df2, on='time', allow_exact_matches=False,
  615. tolerance=pd.Timedelta('10ms'))
  616. expected = pd.DataFrame({
  617. 'time': pd.to_datetime(['2016-07-15 13:30:00.030']),
  618. 'username': ['bob'],
  619. 'version': [np.nan]})
  620. assert_frame_equal(result, expected)
  621. def test_allow_exact_matches_and_tolerance3(self):
  622. # GH 13709
  623. df1 = pd.DataFrame({
  624. 'time': pd.to_datetime(['2016-07-15 13:30:00.030',
  625. '2016-07-15 13:30:00.030']),
  626. 'username': ['bob', 'charlie']})
  627. df2 = pd.DataFrame({
  628. 'time': pd.to_datetime(['2016-07-15 13:30:00.000',
  629. '2016-07-15 13:30:00.030']),
  630. 'version': [1, 2]})
  631. result = pd.merge_asof(df1, df2, on='time', allow_exact_matches=False,
  632. tolerance=pd.Timedelta('10ms'))
  633. expected = pd.DataFrame({
  634. 'time': pd.to_datetime(['2016-07-15 13:30:00.030',
  635. '2016-07-15 13:30:00.030']),
  636. 'username': ['bob', 'charlie'],
  637. 'version': [np.nan, np.nan]})
  638. assert_frame_equal(result, expected)
  639. def test_allow_exact_matches_and_tolerance_forward(self):
  640. # GH14887
  641. left = pd.DataFrame({'a': [1, 5, 10],
  642. 'left_val': ['a', 'b', 'c']})
  643. right = pd.DataFrame({'a': [1, 3, 4, 6, 11],
  644. 'right_val': [1, 3, 4, 6, 11]})
  645. expected = pd.DataFrame({'a': [1, 5, 10],
  646. 'left_val': ['a', 'b', 'c'],
  647. 'right_val': [np.nan, 6, 11]})
  648. result = pd.merge_asof(left, right, on='a', direction='forward',
  649. allow_exact_matches=False, tolerance=1)
  650. assert_frame_equal(result, expected)
  651. def test_allow_exact_matches_and_tolerance_nearest(self):
  652. # GH14887
  653. left = pd.DataFrame({'a': [1, 5, 10],
  654. 'left_val': ['a', 'b', 'c']})
  655. right = pd.DataFrame({'a': [1, 3, 4, 6, 11],
  656. 'right_val': [1, 3, 4, 7, 11]})
  657. expected = pd.DataFrame({'a': [1, 5, 10],
  658. 'left_val': ['a', 'b', 'c'],
  659. 'right_val': [np.nan, 4, 11]})
  660. result = pd.merge_asof(left, right, on='a', direction='nearest',
  661. allow_exact_matches=False, tolerance=1)
  662. assert_frame_equal(result, expected)
  663. def test_forward_by(self):
  664. # GH14887
  665. left = pd.DataFrame({'a': [1, 5, 10, 12, 15],
  666. 'b': ['X', 'X', 'Y', 'Z', 'Y'],
  667. 'left_val': ['a', 'b', 'c', 'd', 'e']})
  668. right = pd.DataFrame({'a': [1, 6, 11, 15, 16],
  669. 'b': ['X', 'Z', 'Y', 'Z', 'Y'],
  670. 'right_val': [1, 6, 11, 15, 16]})
  671. expected = pd.DataFrame({'a': [1, 5, 10, 12, 15],
  672. 'b': ['X', 'X', 'Y', 'Z', 'Y'],
  673. 'left_val': ['a', 'b', 'c', 'd', 'e'],
  674. 'right_val': [1, np.nan, 11, 15, 16]})
  675. result = pd.merge_asof(left, right, on='a', by='b',
  676. direction='forward')
  677. assert_frame_equal(result, expected)
  678. def test_nearest_by(self):
  679. # GH14887
  680. left = pd.DataFrame({'a': [1, 5, 10, 12, 15],
  681. 'b': ['X', 'X', 'Z', 'Z', 'Y'],
  682. 'left_val': ['a', 'b', 'c', 'd', 'e']})
  683. right = pd.DataFrame({'a': [1, 6, 11, 15, 16],
  684. 'b': ['X', 'Z', 'Z', 'Z', 'Y'],
  685. 'right_val': [1, 6, 11, 15, 16]})
  686. expected = pd.DataFrame({'a': [1, 5, 10, 12, 15],
  687. 'b': ['X', 'X', 'Z', 'Z', 'Y'],
  688. 'left_val': ['a', 'b', 'c', 'd', 'e'],
  689. 'right_val': [1, 1, 11, 11, 16]})
  690. result = pd.merge_asof(left, right, on='a', by='b',
  691. direction='nearest')
  692. assert_frame_equal(result, expected)
  693. def test_by_int(self):
  694. # we specialize by type, so test that this is correct
  695. df1 = pd.DataFrame({
  696. 'time': pd.to_datetime(['20160525 13:30:00.020',
  697. '20160525 13:30:00.030',
  698. '20160525 13:30:00.040',
  699. '20160525 13:30:00.050',
  700. '20160525 13:30:00.060']),
  701. 'key': [1, 2, 1, 3, 2],
  702. 'value1': [1.1, 1.2, 1.3, 1.4, 1.5]},
  703. columns=['time', 'key', 'value1'])
  704. df2 = pd.DataFrame({
  705. 'time': pd.to_datetime(['20160525 13:30:00.015',
  706. '20160525 13:30:00.020',
  707. '20160525 13:30:00.025',
  708. '20160525 13:30:00.035',
  709. '20160525 13:30:00.040',
  710. '20160525 13:30:00.055',
  711. '20160525 13:30:00.060',
  712. '20160525 13:30:00.065']),
  713. 'key': [2, 1, 1, 3, 2, 1, 2, 3],
  714. 'value2': [2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8]},
  715. columns=['time', 'key', 'value2'])
  716. result = pd.merge_asof(df1, df2, on='time', by='key')
  717. expected = pd.DataFrame({
  718. 'time': pd.to_datetime(['20160525 13:30:00.020',
  719. '20160525 13:30:00.030',
  720. '20160525 13:30:00.040',
  721. '20160525 13:30:00.050',
  722. '20160525 13:30:00.060']),
  723. 'key': [1, 2, 1, 3, 2],
  724. 'value1': [1.1, 1.2, 1.3, 1.4, 1.5],
  725. 'value2': [2.2, 2.1, 2.3, 2.4, 2.7]},
  726. columns=['time', 'key', 'value1', 'value2'])
  727. assert_frame_equal(result, expected)
  728. def test_on_float(self):
  729. # mimics how to determine the minimum-price variation
  730. df1 = pd.DataFrame({
  731. 'price': [5.01, 0.0023, 25.13, 340.05, 30.78, 1040.90, 0.0078],
  732. 'symbol': list("ABCDEFG")},
  733. columns=['symbol', 'price'])
  734. df2 = pd.DataFrame({
  735. 'price': [0.0, 1.0, 100.0],
  736. 'mpv': [0.0001, 0.01, 0.05]},
  737. columns=['price', 'mpv'])
  738. df1 = df1.sort_values('price').reset_index(drop=True)
  739. result = pd.merge_asof(df1, df2, on='price')
  740. expected = pd.DataFrame({
  741. 'symbol': list("BGACEDF"),
  742. 'price': [0.0023, 0.0078, 5.01, 25.13, 30.78, 340.05, 1040.90],
  743. 'mpv': [0.0001, 0.0001, 0.01, 0.01, 0.01, 0.05, 0.05]},
  744. columns=['symbol', 'price', 'mpv'])
  745. assert_frame_equal(result, expected)
  746. def test_on_specialized_type(self, any_real_dtype):
  747. # see gh-13936
  748. dtype = np.dtype(any_real_dtype).type
  749. df1 = pd.DataFrame({
  750. "value": [5, 2, 25, 100, 78, 120, 79],
  751. "symbol": list("ABCDEFG")},
  752. columns=["symbol", "value"])
  753. df1.value = dtype(df1.value)
  754. df2 = pd.DataFrame({
  755. "value": [0, 80, 120, 125],
  756. "result": list("xyzw")},
  757. columns=["value", "result"])
  758. df2.value = dtype(df2.value)
  759. df1 = df1.sort_values("value").reset_index(drop=True)
  760. result = pd.merge_asof(df1, df2, on="value")
  761. expected = pd.DataFrame(
  762. {"symbol": list("BACEGDF"),
  763. "value": [2, 5, 25, 78, 79, 100, 120],
  764. "result": list("xxxxxyz")
  765. }, columns=["symbol", "value", "result"])
  766. expected.value = dtype(expected.value)
  767. assert_frame_equal(result, expected)
  768. def test_on_specialized_type_by_int(self, any_real_dtype):
  769. # see gh-13936
  770. dtype = np.dtype(any_real_dtype).type
  771. df1 = pd.DataFrame({
  772. "value": [5, 2, 25, 100, 78, 120, 79],
  773. "key": [1, 2, 3, 2, 3, 1, 2],
  774. "symbol": list("ABCDEFG")},
  775. columns=["symbol", "key", "value"])
  776. df1.value = dtype(df1.value)
  777. df2 = pd.DataFrame({
  778. "value": [0, 80, 120, 125],
  779. "key": [1, 2, 2, 3],
  780. "result": list("xyzw")},
  781. columns=["value", "key", "result"])
  782. df2.value = dtype(df2.value)
  783. df1 = df1.sort_values("value").reset_index(drop=True)
  784. result = pd.merge_asof(df1, df2, on="value", by="key")
  785. expected = pd.DataFrame({
  786. "symbol": list("BACEGDF"),
  787. "key": [2, 1, 3, 3, 2, 2, 1],
  788. "value": [2, 5, 25, 78, 79, 100, 120],
  789. "result": [np.nan, "x", np.nan, np.nan, np.nan, "y", "x"]},
  790. columns=["symbol", "key", "value", "result"])
  791. expected.value = dtype(expected.value)
  792. assert_frame_equal(result, expected)
  793. def test_on_float_by_int(self):
  794. # type specialize both "by" and "on" parameters
  795. df1 = pd.DataFrame({
  796. 'symbol': list("AAABBBCCC"),
  797. 'exch': [1, 2, 3, 1, 2, 3, 1, 2, 3],
  798. 'price': [3.26, 3.2599, 3.2598, 12.58, 12.59,
  799. 12.5, 378.15, 378.2, 378.25]},
  800. columns=['symbol', 'exch', 'price'])
  801. df2 = pd.DataFrame({
  802. 'exch': [1, 1, 1, 2, 2, 2, 3, 3, 3],
  803. 'price': [0.0, 1.0, 100.0, 0.0, 5.0, 100.0, 0.0, 5.0, 1000.0],
  804. 'mpv': [0.0001, 0.01, 0.05, 0.0001, 0.01, 0.1, 0.0001, 0.25, 1.0]},
  805. columns=['exch', 'price', 'mpv'])
  806. df1 = df1.sort_values('price').reset_index(drop=True)
  807. df2 = df2.sort_values('price').reset_index(drop=True)
  808. result = pd.merge_asof(df1, df2, on='price', by='exch')
  809. expected = pd.DataFrame({
  810. 'symbol': list("AAABBBCCC"),
  811. 'exch': [3, 2, 1, 3, 1, 2, 1, 2, 3],
  812. 'price': [3.2598, 3.2599, 3.26, 12.5, 12.58,
  813. 12.59, 378.15, 378.2, 378.25],
  814. 'mpv': [0.0001, 0.0001, 0.01, 0.25, 0.01, 0.01, 0.05, 0.1, 0.25]},
  815. columns=['symbol', 'exch', 'price', 'mpv'])
  816. assert_frame_equal(result, expected)
  817. def test_merge_datatype_error(self):
  818. """ Tests merge datatype mismatch error """
  819. msg = r'merge keys \[0\] object and int64, must be the same type'
  820. left = pd.DataFrame({'left_val': [1, 5, 10],
  821. 'a': ['a', 'b', 'c']})
  822. right = pd.DataFrame({'right_val': [1, 2, 3, 6, 7],
  823. 'a': [1, 2, 3, 6, 7]})
  824. with pytest.raises(MergeError, match=msg):
  825. merge_asof(left, right, on='a')
  826. @pytest.mark.parametrize('func', [lambda x: x, lambda x: to_datetime(x)],
  827. ids=['numeric', 'datetime'])
  828. @pytest.mark.parametrize('side', ['left', 'right'])
  829. def test_merge_on_nans(self, func, side):
  830. # GH 23189
  831. msg = "Merge keys contain null values on {} side".format(side)
  832. nulls = func([1.0, 5.0, np.nan])
  833. non_nulls = func([1.0, 5.0, 10.])
  834. df_null = pd.DataFrame({'a': nulls, 'left_val': ['a', 'b', 'c']})
  835. df = pd.DataFrame({'a': non_nulls, 'right_val': [1, 6, 11]})
  836. with pytest.raises(ValueError, match=msg):
  837. if side == 'left':
  838. merge_asof(df_null, df, on='a')
  839. else:
  840. merge_asof(df, df_null, on='a')
  841. def test_merge_by_col_tz_aware(self):
  842. # GH 21184
  843. left = pd.DataFrame(
  844. {'by_col': pd.DatetimeIndex(['2018-01-01']).tz_localize('UTC'),
  845. 'on_col': [2], 'values': ['a']})
  846. right = pd.DataFrame(
  847. {'by_col': pd.DatetimeIndex(['2018-01-01']).tz_localize('UTC'),
  848. 'on_col': [1], 'values': ['b']})
  849. result = pd.merge_asof(left, right, by='by_col', on='on_col')
  850. expected = pd.DataFrame([
  851. [pd.Timestamp('2018-01-01', tz='UTC'), 2, 'a', 'b']
  852. ], columns=['by_col', 'on_col', 'values_x', 'values_y'])
  853. assert_frame_equal(result, expected)